/// The Lexer is an abstract base class providing all the facilities that the
/// Parser expects. It goes through the stream one token at a time and keeps
-/// track of the location in the file for debugging purpose.
+/// track of the location in the file for debugging purposes.
/// It relies on a subclass to provide a `readNextLine()` method. The subclass
/// can proceed by reading the next line from the standard input or from a
/// memory mapped file.
class Lexer {
public:
/// Create a lexer for the given filename. The filename is kept only for
- /// debugging purpose (attaching a location to a Token).
+ /// debugging purposes (attaching a location to a Token).
Lexer(std::string filename)
: lastLocation(
{std::make_shared<std::string>(std::move(filename)), 0, 0}) {}
# Operation Canonicalization in MLIR
-Canonicalization is an important part of compiler IR design - it makes it easier
-to implement reliable compiler transformations, be able to reason about what is
-better or worse in the code, and forces interesting discussions about what the
-goals of a particular level of IR are interested in. Dan Gohman wrote
+Canonicalization is an important part of compiler IR design: it makes it easier
+to implement reliable compiler transformations and to reason about what is
+better or worse in the code, and it forces interesting discussions about the
+goals of a particular level of IR. Dan Gohman wrote
[an article](https://sunfishcode.github.io/blog/2018/10/22/Canonicalization.html)
-exploring these issues, it is worth reading if you're not familiar with these
+exploring these issues; it is worth reading if you're not familiar with these
concepts.
Most compilers have canonicalization passes, and sometimes they have many
multi-level IR, we can provide a single canonicalization infrastructure and
reuse it across many different IRs that it represents. This document describes
the general approach, global canonicalizations performed, and provides sections
-to capture IR specific rules for reference.
+to capture IR-specific rules for reference.
## General Design
## Style guide
MLIR follows the [LLVM style](https://llvm.org/docs/CodingStandards.html) guide.
-We also adhere to the following, that deviates from, or isn't specified in the
-LLVM style guide:
+We also adhere to the following (which deviate from or are not specified in the
+LLVM style guide):
* Adopts [camelBack](https://llvm.org/docs/Proposals/VariableNames.html);
* Except for IR units (Region, Block, and Operation), non-nullable output
The conversion target is the formal definition of what is considered to be legal
during the conversion process. The final operations generated by the conversion
framework must be marked as legal on the `ConversionTarget` for the rewrite to
-be a success. Existing operations need not always be legal though, see the
-different conversion modes for why. Operations, and dialects, may be marked with
+be a success. Existing operations need not always be legal, though; see the
+different conversion modes for why. Operations and dialects may be marked with
any of the provided legality actions below:
* Legal
- This action signals that no instance of a given operation is legal.
Operations marked as `illegal` must always be converted for the
conversion to be successful. This action also allows for selectively
- marking specific operations, in an otherwise legal dialect, as illegal.
+ marking specific operations as illegal in an otherwise legal dialect.
An example conversion target is shown below:
## Type Conversion
It is sometimes necessary as part of a conversion to convert the set types of
-being operated on. In these cases a `TypeConverter` object may be defined that
-details how types should be converted. The `TypeConverter` is used by patterns,
-and the general conversion infrastructure, to convert the signatures of blocks
+being operated on. In these cases, a `TypeConverter` object may be defined that
+details how types should be converted. The `TypeConverter` is used by patterns
+and by the general conversion infrastructure to convert the signatures of blocks
and regions.
### Type Converter
As stated above, the `TypeConverter` contains several hooks for detailing how to
-convert types, several of these hooks are detailed below:
+convert types. Several of these hooks are detailed below:
```c++
class TypeConverter {
# Introduction to MLIR Interfaces
-MLIR is generic and very extensible, it allows for opaquely representing many
-different dialects that have their own operations, attributes, types, etc. This
-allows for dialects to be very expressive in their semantics, and allows for
-MLIR to capture many different levels of abstraction. The downside to this is
-that transformations and analyses must be extremely conservative about the
-operations that they encounter, and must special case the different dialects
-that they support. To combat this, MLIR provides the concept of `interfaces`.
+MLIR is generic and very extensible; it allows for opaquely representing many
+different dialects that have their own operations, attributes, types, and so on.
+This allows for dialects to be very expressive in their semantics and for MLIR
+to capture many different levels of abstraction. The downside to this is that
+transformations and analyses must be extremely conservative about the operations
+that they encounter, and must special-case the different dialects that they
+support. To combat this, MLIR provides the concept of `interfaces`.
## Motivation
Interfaces provide a generic way of interacting with the IR. The goal is to be
able to express transformations/analyses in terms of these interfaces without
encoding specific knowledge about the exact operation or dialect involved. This
-will make the compiler more extensible by being able to add new dialects and
+makes the compiler more extensible by allowing the addition of new dialects and
operations in a decoupled way with respect to the implementation of
transformations/analyses.
### Dialect Interfaces
-Dialect interfaces are generally useful for transformation passes, or analyses,
+Dialect interfaces are generally useful for transformation passes or analyses
that want to opaquely operate on operations, even *across* dialects. These
-interfaces generally involve wide coverage over the entire dialect, and are only
+interfaces generally involve wide coverage over the entire dialect and are only
used for a handful of transformations/analyses. In these cases, registering the
interface directly on each operation is overly complex and cumbersome. The
interface is not core to the operation, just to the specific transformation. An
example of where this type of interface would be used is inlining. Inlining
-generally queries high level information about the operations within a dialect,
+generally queries high-level information about the operations within a dialect,
like legality and cost modeling, that often is not specific to one operation.
A dialect interface can be defined by inheriting from the CRTP base class
`DialectInterfaceBase::Base`. This class provides the necessary utilities for
registering an interface with the dialect so that it can be looked up later.
-Once the interface has been defined, dialects can override it using dialect
-specific information. The interfaces defined by a dialect are registered in a
-similar mechanism to Attributes, Operations, Types, etc.
+Once the interface has been defined, dialects can override it using
+dialect-specific information. The interfaces defined by a dialect are registered
+in a similar mechanism to Attributes, Operations, Types, etc.
```c++
/// Define an Inlining interface to allow for dialects to opt-in.
/// 'dest' that is attached to an operation registered to the current dialect.
/// 'valueMapping' contains any remapped values from within the 'src' region.
/// This can be used to examine what values will replace entry arguments into
- /// the 'src' region for example.
+ /// the 'src' region, for example.
virtual bool isLegalToInline(Region *dest, Region *src,
BlockAndValueMapping &valueMapping) const {
return false;
### Operation Interfaces
Operation interfaces, as the name suggests, are those registered at the
-Operation level. These interfaces provide an opaque view into derived
-operations, by providing a virtual interface that must be implemented. As an
-example, the `Linalg` dialect may implement an interface that provides general
-queries about some of the dialects library operations. These queries may provide
-things like: the number of parallel loops, the number of inputs and outputs,
-etc.
+Operation level. These interfaces provide an opaque view into derived operations
+by providing a virtual interface that must be implemented. As an example, the
+`Linalg` dialect may implement an interface that provides general queries about
+some of the dialects library operations. These queries may provide things like:
+the number of parallel loops; the number of inputs and outputs; etc.
Operation interfaces are defined by overriding the CRTP base class
-`OpInterface`. This class takes as a template parameter, a `Traits` class that
+`OpInterface`. This class takes, as a template parameter, a `Traits` class that
defines a `Concept` and a `Model` class. These classes provide an implementation
of concept-based polymorphism, where the Concept defines a set of virtual
methods that are overridden by the Model that is templated on the concrete
MLIR has a simple and unambiguous grammar, allowing it to reliably round-trip
through a textual form. This is important for development of the compiler - e.g.
-understanding the state of code as it is being transformed and for writing test
+for understanding the state of code as it is being transformed and writing test
cases.
This document describes the grammar using
## Dialects
-Dialects are the mechanism in which to engage with and extend the MLIR
+Dialects are the mechanism by which to engage with and extend the MLIR
ecosystem. They allow for defining new [operations](#operations), as well as
[attributes](#attributes) and [types](#type-system). Each dialect is given a
unique `namespace` that is prefixed to each defined attribute/operation/type.
MLIR allows for multiple dialects, even those outside of the main tree, to
co-exist together within one module. Dialects are produced and consumed by
certain passes. MLIR provides a [framework](DialectConversion.md) to convert
-between, and within different dialects.
+between, and within, different dialects.
A few of the dialects supported by MLIR:
MLIR introduces a uniform concept called _operations_ to enable describing many
different levels of abstractions and computations. Operations in MLIR are fully
-extensible (there is no fixed list of operations), and have application-specific
+extensible (there is no fixed list of operations) and have application-specific
semantics. For example, MLIR supports
[target-independent operations](Dialects/Standard.md#memory-operations),
[affine operations](Dialects/Affine.md), and
### Terminator Operations
-These are a special category of operations that *must* terminate a block, for
-example [branches](Dialects/Standard.md#terminator-operations). These operations
-may also have a list of successors ([blocks](#blocks) and their arguments).
+These are a special category of operations that *must* terminate a block, e.g.
+[branches](Dialects/Standard.md#terminator-operations). These operations may
+also have a list of successors ([blocks](#blocks) and their arguments).
Example:
### Control and Value Scoping
Regions provide nested control isolation: it is impossible to branch to a block
-within a region from outside it, or to branch from within a region to a block
-outside it. Similarly it provides a natural scoping for value visibility: SSA
-values defined in a region don't escape to the enclosing region if any. By
-default, a region can reference values defined outside of the region, whenever
-it would have been legal to use them as operands to the enclosing operation.
+within a region from outside it or to branch from within a region to a block
+outside it. Similarly, it provides a natural scoping for value visibility: SSA
+values defined in a region don't escape to the enclosing region, if any. By
+default, a region can reference values defined outside of the region whenever it
+would have been legal to use them as operands to the enclosing operation.
Example:
Each SSA value in MLIR has a type defined by the type system below. There are a
number of primitive types (like integers) and also aggregate types for tensors
-and memory buffers. MLIR standard types do not include structures, arrays, or
-dictionaries.
+and memory buffers. MLIR [standard types](#standard-types) do not include
+structures, arrays, or dictionaries.
-MLIR has an open type system (there is no fixed list of types), and types may
-have application-specific semantics. For example, MLIR supports a set of
-[standard types](#standard-types) as well as [dialect types](#dialect-types).
+MLIR has an open type system (i.e. there is no fixed list of types), and types
+may have application-specific semantics. For example, MLIR supports a set of
+[dialect types](#dialect-types).
``` {.ebnf}
type ::= type-alias | dialect-type | standard-type
expected attributes, their structure, and their interpretation are all
contextually dependent on what they are attached to.
-There are two main classes of attributes; dependent and dialect. Dependent
-attributes derive their structure and meaning from what they are attached to,
+There are two main classes of attributes: dependent and dialect. Dependent
+attributes derive their structure and meaning from what they are attached to;
e.g., the meaning of the `index` attribute on a `dim` operation is defined by
the `dim` operation. Dialect attributes, on the other hand, derive their context
and meaning from a specific dialect. An example of a dialect attribute may be a
[TOC]
This tutorial runs through the implementation of a basic toy language on top of
-MLIR. The goal of this tutorial is to introduce the concepts of MLIR, and
-especially how *dialects* can help easily support language specific constructs
-and transformations, while still offering an easy path to lower to LLVM or other
-codegen infrastructure. This tutorial is based on the model of the
+MLIR. The goal of this tutorial is to introduce the concepts of MLIR; in
+particular, how [dialects](../../LangRef.md#dialects) can help easily support
+language specific constructs and transformations while still offering an easy
+path to lower to LLVM or other codegen infrastructure. This tutorial is based on
+the model of the
[LLVM Kaleidoscope Tutorial](https://llvm.org/docs/tutorial/MyFirstLanguageFrontend/index.html).
+This tutorial assumes you have cloned and built MLIR; if you have not yet done
+so, see
+[Getting started with MLIR](https://github.com/tensorflow/mlir#getting-started-with-mlir).
+
## The Chapters
This tutorial is divided in the following chapters:
-- [Chapter #1](Ch-1.md): Introduction to the Toy language, and the definition
+- [Chapter #1](Ch-1.md): Introduction to the Toy language and the definition
of its AST.
- [Chapter #2](Ch-2.md): Traversing the AST to emit a dialect in MLIR,
introducing base MLIR concepts. Here we show how to start attaching
semantics to our custom operations in MLIR.
- [Chapter #3](Ch-3.md): High-level language-specific optimization using
pattern rewriting system.
-- [Chapter #4](Ch-4.md): Writing generic dialect independent transformations
+- [Chapter #4](Ch-4.md): Writing generic dialect-independent transformations
with Interfaces. Here we will show how to plug dialect specific information
into generic transformations like shape inference and inlining.
- [Chapter #5](Ch-5.md): Partially lowering to lower-level dialects. We'll
This tutorial will be illustrated with a toy language that we’ll call “Toy”
(naming is hard...). Toy is a tensor-based language that allows you to define
-functions, some math computation, and print results.
+functions, perform some math computation, and print results.
Given that we want to keep things simple, the codegen will be limited to tensors
-of rank <= 2 and the only datatype in Toy is a 64-bit floating point type (aka
+of rank <= 2, and the only datatype in Toy is a 64-bit floating point type (aka
‘double’ in C parlance). As such, all values are implicitly double precision,
`Values` are immutable (i.e. every operation returns a newly allocated value),
-and deallocation is automatically managed. But enough with the long description,
+and deallocation is automatically managed. But enough with the long description;
nothing is better than walking through an example to get a better understanding:
```Toy {.toy}
Type checking is statically performed through type inference; the language only
requires type declarations to specify tensor shapes when needed. Functions are
-generic: their parameters are unranked (in other words we know these are tensors
-but we don't know how many dimensions or the size of the dimensions). They are
-specialized for every newly discovered signature at call sites. Let's revisit
-the previous example by adding a user-defined function:
+generic: their parameters are unranked (in other words, we know these are
+tensors, but we don't know their dimensions). They are specialized for every
+newly discovered signature at call sites. Let's revisit the previous example by
+adding a user-defined function:
```Toy {.toy}
# User defined generic function that operates on unknown shaped arguments.
## The AST
-The AST is fairly straightforward from the above code, here is a dump of it:
+The AST from the above code is fairly straightforward; here is a dump of it:
```
Module:
```
You can reproduce this result and play with the example in the
-`examples/toy/Ch1/` directory, try running `path/to/BUILD/bin/toyc-ch1
-test/ast.toy -emit=ast`.
+`examples/toy/Ch1/` directory; try running `path/to/BUILD/bin/toyc-ch1
+test/Examples/Toy/Ch1/ast.toy -emit=ast`.
-The code for the lexer is fairly straightforward, it is all in a single header:
+The code for the lexer is fairly straightforward; it is all in a single header:
`examples/toy/Ch1/include/toy/Lexer.h`. The parser can be found in
-`examples/toy/Ch1/include/toy/Parser.h`, it is a recursive descent parser. If
+`examples/toy/Ch1/include/toy/Parser.h`; it is a recursive descent parser. If
you are not familiar with such a Lexer/Parser, these are very similar to the
LLVM Kaleidoscope equivalent that are detailed in the first two chapters of the
[Kaleidoscope Tutorial](https://llvm.org/docs/tutorial/MyFirstLanguageFrontend/LangImpl02.html).
[TOC]
-Now that we're familiar with our language and the AST, let see how MLIR can help
-to compile Toy.
+Now that we're familiar with our language and the AST, let's see how MLIR can
+help to compile Toy.
-## Introduction: Multi-Level IR
+## Introduction: Multi-Level Intermediate Representation
-Other compilers like LLVM (see the
-[Kaleidoscope tutorial](https://llvm.org/docs/tutorial/MyFirstLanguageFrontend/index.html))
-offer a fixed set of predefined types and, usually *low-level* / RISC-like,
+Other compilers, like LLVM (see the
+[Kaleidoscope tutorial](https://llvm.org/docs/tutorial/MyFirstLanguageFrontend/index.html)),
+offer a fixed set of predefined types and (usually *low-level* / RISC-like)
instructions. It is up to the frontend for a given language to perform any
-language specific type-checking, analysis, or transformation before emitting
-LLVM IR. For example, clang will use its AST to perform static analysis but also
-transformations like C++ template instantiation through AST cloning and rewrite.
-Finally, languages with construction at a higher-level than C/C++ may require
-non-trivial lowering from their AST to generate LLVM IR.
+language-specific type-checking, analysis, or transformation before emitting
+LLVM IR. For example, Clang will use its AST to perform not only static analysis
+but also transformations, such as C++ template instantiation through AST cloning
+and rewrite. Finally, languages with construction at a higher-level than C/C++
+may require non-trivial lowering from their AST to generate LLVM IR.
As a consequence, multiple frontends end up reimplementing significant pieces of
infrastructure to support the need for these analyses and transformation. MLIR
-addresses this issue by being designed for extensibility. As such, there are
-little to none pre-defined instructions (*operations* in MLIR terminology) or
-types.
+addresses this issue by being designed for extensibility. As such, there are few
+pre-defined instructions (*operations* in MLIR terminology) or types.
## Interfacing with MLIR
[Language reference](../../LangRef.md)
MLIR is designed to be a completely extensible infrastructure; there is no
-closed set of attributes (think always constant metadata), operations, or types.
-MLIR supports this extensibility with the concept of
+closed set of attributes (think: constant metadata), operations, or types. MLIR
+supports this extensibility with the concept of
[Dialects](../../LangRef.md#dialects). Dialects provide a grouping mechanism for
abstraction under a unique `namespace`.
all of the core IR structures in LLVM: instructions, globals (like functions),
modules, etc.
-Here is the MLIR assembly for the Toy 'transpose' operations:
+Here is the MLIR assembly for the Toy `transpose` operations:
```mlir
%t_tensor = "toy.transpose"(%tensor) {inplace = true} : (tensor<2x3xf64>) -> tensor<3x2xf64>
- `%t_tensor`
- * The name given to the result defined by this operation. An operation may
- define zero or more results (we will limit ourselves to single result
- operations in the context of Toy), which are SSA values. The name is
- used during parsing but is not persistent (e.g., it is not tracked in
- the in memory representation of the SSA value).
+ * The name given to the result defined by this operation (which includes
+ [a prefixed sigil to avoid collisions](../../LangRef.md#identifiers-and-keywords)).
+ An operation may define zero or more results (in the context of Toy, we
+ will limit ourselves to single-result operations), which are SSA values.
+ The name is used during parsing but is not persistent (e.g., it is not
+ tracked in the in-memory representation of the SSA value).
- `"toy.transpose"`
- `{ inplace = true }`
* A dictionary of zero or more attributes, which are special operands that
- are always constant. Here we define a boolean attribute named 'inplace',
+ are always constant. Here we define a boolean attribute named 'inplace'
that has a constant value of true.
- `(tensor<2x3xf64) -> tensor<3x2xf64>`
- A list of SSA operand values.
- A list of [types](../../LangRef.md#type-system) for result values.
- A list of [attributes](../../LangRef.md#attributes).
-- A list of successors [blocks](../../LangRef.md#blocks) (for branches
+- A list of successors [blocks](../../LangRef.md#blocks) (for branches,
mostly).
- A list of [regions](../../LangRef.md#regions) (for structural operations
like functions).
-Finally, in MLIR every operation has a mandatory source location associated with
-it. Contrary to LLVM where debug info locations are metadata and can be dropped,
-in MLIR the location is a core requirement which translates in APIs manipulating
-operations requiring it. Dropping a location becomes an explicit choice and
-cannot happen by mistake.
+In MLIR, every operation has a mandatory source location associated with it.
+Contrary to LLVM, where debug info locations are metadata and can be dropped, in
+MLIR, the location is a core requirement, and APIs depend on and manipulate it.
+Dropping a location is thus an explicit choice which cannot happen by mistake.
### Opaque API
MLIR is designed to be a completely extensible system, and as such, the
infrastructure has the capability to opaquely represent all of its core
components: attributes, operations, types, etc. This allows MLIR to parse,
-represent, and round-trip any valid IR. For example, we could place our toy
-operation from above into an `.mlir` file and round-trip through *mlir-opt*
-without registering anything:
+represent, and [round-trip](../../Glossary.md#round-trip) any valid IR. For
+example, we could place our Toy operation from above into an `.mlir` file and
+round-trip through *mlir-opt* without registering anything:
```mlir
func @toy_func(%tensor: tensor<2x3xf64>) -> tensor<3x2xf64> {
}
```
-In the cases of unregistered attributes, operations, types, MLIR will enforce
-some structural constraints (SSA, block termination, etc.) but otherwise they
-are completely opaque. This can be useful for bootstrapping purposes, but it is
-generally advised against. Opaque operations must be treated conservatively by
-transformations and analyses, and are much harder to construct and manipulate.
+In the cases of unregistered attributes, operations, and types, MLIR will
+enforce some structural constraints (SSA, block termination, etc.), but
+otherwise they are completely opaque. This can be useful for bootstrapping
+purposes, but it is generally advised against. Opaque operations must be treated
+conservatively by transformations and analyses, and they are much harder to
+construct and manipulate.
This handling can be observed by crafting what should be an invalid IR for Toy
-and see it round-trip without tripping the verifier:
+and seeing it round-trip without tripping the verifier:
```mlir
// RUN: toyc %s -emit=mlir
}
```
-There are multiple problems here: the `toy.print` operation is not a terminator,
-it should take an operand, and it shouldn't return any values. In the next
+There are multiple problems here: the `toy.print` operation is not a terminator;
+it should take an operand; and it shouldn't return any values. In the next
section, we will register our dialect and operations with MLIR, plug into the
verifier, and add nicer APIs to manipulate our operations.
```
Any new `MLIRContext` created from now on will contain an instance of the Toy
-dialect, and invoke specific hooks for things like parsing attributes and types.
+dialect and invoke specific hooks for things like parsing attributes and types.
## Defining Toy Operations
`Operation*`. This means that when we define our Toy operations, we are actually
providing a clean interface for building and interfacing with the `Operation`
class; this is why our `ConstantOp` defines no class fields. Therefore, we
-always pass these classes around by-value, instead of by reference or pointer
-(passing by-value is a common idiom and applies similarly to attributes, types,
-etc). We can always get an instance of our toy operation by using LLVM's casting
-infrastructure:
+always pass these classes around by value, instead of by reference or pointer
+(*passing by value* is a common idiom and applies similarly to attributes,
+types, etc). We can always get an instance of our toy operation by using LLVM's
+casting infrastructure:
```c++
void processConstantOp(mlir::Operation *op) {
Lets see how to define the ODS equivalent of our ConstantOp:
The first thing to do is to define a link to the Toy dialect that we defined in
-c++. This is used to link all of the operations that we will define, to our
+C++. This is used to link all of the operations that we will define to our
dialect:
```tablegen
// provided in `ToyDialect::getDialectNamespace`.
let name = "toy";
- // The c++ namespace that the dialect class definition resides in.
+ // The C++ namespace that the dialect class definition resides in.
let cppNamespace = "toy";
}
```
-Now that we have defined a link to the toy dialect, we can start defining
+Now that we have defined a link to the Toy dialect, we can start defining
operations. Operations in ODS are defined by inheriting from the `Op` class. To
simplify our operation definitions, we will define a base class for operations
in the Toy dialect.
```
With all of the preliminary pieces defined, we can begin to define the constant
-operation:
+operation.
We define a toy operation by inheriting from our base 'Toy_Op' class above. Here
we provide the mnemonic and a list of traits for the operation. The
`ConstantOp::getOperationName` without the dialect prefix; `toy.`. The constant
operation here is also marked as 'NoSideEffect'. This is an ODS trait, and
matches one-to-one with the trait we providing when defining `ConstantOp`:
-`mlir::OpTrait::HasNoSideEffect`. Missing here from our c++ definition are the
-`ZeroOperands` and `OneResult` traits, these will be automatically inferred
+`mlir::OpTrait::HasNoSideEffect`. Missing here from our C++ definition are the
+`ZeroOperands` and `OneResult` traits; these will be automatically inferred
based upon the `arguments` and `results` fields we define later.
```tablegen
#### Adding Documentation
-The next step after defining the operation, is to document it. Operations may
+The next step after defining the operation is to document it. Operations may
provide
[`summary` and `description`](../../OpDefinitions.md#operation-documentation)
fields to describe the semantics of the operation. This information is useful
-for users of the dialect, and can even be used to auto-generate markdown
+for users of the dialect and can even be used to auto-generate Markdown
documents.
```tablegen
#### Verifying Operation Semantics
-At this point we've already covered a majority of the original c++ operation
+At this point we've already covered a majority of the original C++ operation
definition. The next piece to define is the verifier. Luckily, much like the
named accessor, the ODS framework will automatically generate a lot of the
necessary verification logic based upon the constraints we have given. This
input attribute `value`. In many cases, additional verification is not even
necessary for ODS operations. To add additional verification logic, an operation
can override the [`verifier`](../../OpDefinitions.md#custom-verifier-code)
-field. The `verifier` field allows for defining a c++ code blob that will be run
-as part of ConstantOp::verify. This blob can assume that all of the other
+field. The `verifier` field allows for defining a C++ code blob that will be run
+as part of `ConstantOp::verify`. This blob can assume that all of the other
invariants of the operation have already been verified:
```tablegen
let results = (outs F64Tensor);
// Add additional verification logic to the constant operation. Here we invoke
- // a static `verify` method in a c++ source file. This codeblock is executed
+ // a static `verify` method in a C++ source file. This codeblock is executed
// inside of ConstantOp::verify, so we can use `this` to refer to the current
// operation instance.
let verifier = [{ return ::verify(*this); }];
#### Attaching `build` Methods
-The final missing component here from our original c++ example are the `build`
+The final missing component here from our original C++ example are the `build`
methods. ODS can generate some simple build methods automatically, and in this
case it will generate our first build method for us. For the rest, we define the
[`builders`](../../OpDefinitions.md#custom-builder-methods) field. This field
takes a list of `OpBuilder` objects that take a string corresponding to a list
-of c++ parameters, as well as an optional code block that can be used to specify
+of C++ parameters, as well as an optional code block that can be used to specify
the implementation inline.
```tablegen
// operation instance.
let verifier = [{ return ::verify(*this); }];
- // Add custom build methods for the constant operation. These method populates
+ // Add custom build methods for the constant operation. These methods populate
// the `state` that MLIR uses to create operations, i.e. these are used when
// using `builder.create<ConstantOp>(...)`.
let builders = [
} loc("test/codegen.toy":0:0)
```
-You can build `toyc-ch2` and try yourself: `toyc-ch2 test/codegen.toy -emit=mlir
--mlir-print-debuginfo`. We can also check our RoundTrip: `toyc-ch2
-test/codegen.toy -emit=mlir -mlir-print-debuginfo 2> codegen.mlir` followed by
-`toyc-ch2 codegen.mlir -emit=mlir`. You should also use `mlir-tblgen` on the
-final definition file and study the generated C++ code.
+You can build `toyc-ch2` and try yourself: `toyc-ch2
+test/Examples/Toy/Ch2/codegen.toy -emit=mlir -mlir-print-debuginfo`. We can also
+check our RoundTrip: `toyc-ch2 test/Examples/Toy/Ch2/codegen.toy -emit=mlir
+-mlir-print-debuginfo 2> codegen.mlir` followed by `toyc-ch2 codegen.mlir
+-emit=mlir`. You should also use `mlir-tblgen` on the final definition file and
+study the generated C++ code.
-At this point MLIR knows about our Toy dialect and operations. In the
-[next chapter](Ch-3.md) we will leverage our new dialect to implement some
+At this point, MLIR knows about our Toy dialect and operations. In the
+[next chapter](Ch-3.md), we will leverage our new dialect to implement some
high-level language-specific analyses and transformations for the Toy language.
[TOC]
Creating a dialect that closely represents the semantics of an input language
-enables analyses, transformations and optimizations in MLIR that require high
-level language information and are generally performed on the language AST. For
-example, `clang` has a fairly
+enables analyses, transformations and optimizations in MLIR that require
+high-level language information and are generally performed on the language AST.
+For example, `clang` has a fairly
[heavy mechanism](https://clang.llvm.org/doxygen/classclang_1_1TreeTransform.html)
for performing template instantiation in C++.
There are two methods that can be used to implement pattern-match
transformations: 1. Imperative, C++ pattern-match and rewrite 2. Declarative,
-rule-based pattern-match and rewrite using
-[Table-driven Declarative Rewrite Rule](../../DeclarativeRewrites.md) (DRR).
-Note that the use of DRR requires that the operations be defined using ODS as
-described in [Chapter 2](../Ch-2.md).
+rule-based pattern-match and rewrite using table-driven
+[Declarative Rewrite Rules](../../DeclarativeRewrites.md) (DRR). Note that the
+use of DRR requires that the operations be defined using ODS, as described in
+[Chapter 2](Ch-2.md).
# Optimize Transpose using C++ style pattern-match and rewrite
```
This is a good example of a transformation that is trivial to match on the Toy
-IR but that would be quite hard for LLVM to figure. For example today clang
-can't optimize away the temporary array and the computation with the naive
-transpose expressed with these loops:
+IR but that would be quite hard for LLVM to figure. For example, today Clang
+can't optimize away the temporary array, and the computation with the naive
+transpose is expressed with these loops:
```c++
#define N 100
pm.addNestedPass<mlir::FuncOp>(mlir::createCanonicalizerPass());
```
-Finally, we can try to run `toyc-ch3 test/transpose_transpose.toy -emit=mlir -opt`
-and observe our pattern in action:
+Finally, we can run `toyc-ch3 test/transpose_transpose.toy -emit=mlir -opt` and
+observe our pattern in action:
```MLIR(.mlir)
func @transpose_transpose(%arg0: tensor<*xf64>) -> tensor<*xf64> {
}
```
-As expected we now directly return the function argument, bypassing any
-transpose operation. However one of the transpose hasn't been eliminated. That
-is not ideal! What happened is that our pattern replaced the last transform with
-the function input and left behind the now dead transpose input. The
-Canonicalizer knows to cleanup dead operations, however MLIR conservatively
-assumes that operations may have side-effects. We can fix it by adding a new
-trait, `NoSideEffect`, to our `TransposeOp`:
+As expected, we now directly return the function argument, bypassing any
+transpose operation. However, one of the transposes still hasn't been
+eliminated. That is not ideal! What happened is that our pattern replaced the
+last transform with the function input and left behind the now dead transpose
+input. The Canonicalizer knows to clean up dead operations; however, MLIR
+conservatively assumes that operations may have side-effects. We can fix this by
+adding a new trait, `NoSideEffect`, to our `TransposeOp`:
```TableGen(.td):
def TransposeOp : Toy_Op<"transpose", [NoSideEffect]> {...}
```
-Let's retry now `toyc test/transpose_transpose.toy -emit=mlir -opt`:
+Let's retry now `toyc-ch3 test/transpose_transpose.toy -emit=mlir -opt`:
```MLIR(.mlir)
func @transpose_transpose(%arg0: tensor<*xf64>) -> tensor<*xf64> {
}
```
-Perfect! No `transpose` operation is left, the code is optimal.
+Perfect! No `transpose` operation is left - the code is optimal.
In the next section, we use DRR for pattern match optimizations associated with
the Reshape op.
# Optimize Reshapes using DRR
-Declarative, rule-based pattern-match and rewrite or DRR is an operation
+Declarative, rule-based pattern-match and rewrite (DRR) is an operation
DAG-based declarative rewriter that provides a table-based syntax for
pattern-match and rewrite rules:
```
The automatically generated C++ code corresponding to each of the DRR patterns
-can be found under $BUILD_DIR/projects/mlir/examples/toy/Ch3/ToyCombine.inc.
+can be found under path/to/BUILD/projects/mlir/examples/toy/Ch3/ToyCombine.inc.
DRR also provides a method for adding argument constraints when the
transformation is conditional on some properties of the arguments and results.
}
```
-We can try to run `toyc-ch3 test/trivialReshape.toy -emit=mlir -opt` and observe our
-pattern in action:
+We can try to run `toyc-ch3 test/trivialReshape.toy -emit=mlir -opt` and observe
+our pattern in action:
```MLIR(.mlir)
module {
## Background: Grappling with an Extensible IR
Through dialects, MLIR allows for the representation of many different levels of
-abstraction; with the Toy dialect that we have previously defined being one such
+abstraction; the Toy dialect that we have previously defined is one such
example. Though these different dialects may represent different abstractions,
-there are often a set of common transformations and analyses that we would like
+there is often a set of common transformations and analyses that we would like
to perform. The problem that arises is that naively implementing each
transformation for each dialect leads to large amounts of code duplication, as
-the internal algorithms are generally very similar if not the same. We would
+the internal algorithms are generally very similar, if not the same. We would
like to provide the ability for transformations to opaquely hook into dialects
like Toy to get the information they need.
-MLIR provides a set of always available hooks for certain core transformations,
-as seen in the [previous chapter](Ch-3.md) when we registered some
-canonicalizations via a hook on our operations: `getCanonicalizationPatterns`,
-but these types of hooks don't really scale well. Therefore a more generic
-solution to make the MLIR infrastructure as extensible as the representation was
-designed, in the form of Interfaces. [Interfaces](../../Interfaces.md) provide a
-generic mechanism for dialects and operations to provide information to a
+MLIR provides a set of always available-hooks for certain core transformations,
+as seen in the [previous chapter](Ch-3.md), where we registered some
+canonicalizations via a hook on our operations (`getCanonicalizationPatterns`).
+However, these types of hooks don't really scale well. Therefore, a more generic
+solution was designed, in the form of [interfaces](../../Interfaces.md), to make
+the MLIR infrastructure as extensible as the representation. Interfaces provide
+a generic mechanism for dialects and operations to provide information to a
transformation or analysis.
## Shape Inference: Preparing for Code Generation
we were to introduce more control flow in the language. Another approach would
be function specialization, where every call site with new argument shapes
duplicates the called function and specializes it. The approach we take for Toy
-is to inline all of the function calls, and then perform a simple
-intra-procedural shape propagation.
+is to inline all of the function calls, then perform intraprocedural shape
+propagation.
### Inlining
dialect, but that can become quite complicated depending on the level of
complexity that we want. Disregarding cost modeling, the pure structural
transformation is already complex to implement from scratch. Thankfully, MLIR
-provides already provides a generic inliner algorithm that dialects can plug
-into. All we need to do in Toy, is to provide the
-[interfaces](../../Interfaces.md) for the inliner to hook into.
+provides a generic inliner algorithm that dialects can plug into. All we need to
+do in Toy is to provide the [interfaces](../../Interfaces.md) for the inliner to
+hook into.
-The first thing we need to do, is to define the constraints on inlining
+The first thing we need to do is to define the constraints on inlining
operations in the Toy dialect. This information is provided through a
-[dialect interface](../../Interfaces.md#dialect-interfaces). A dialect interface
-is essentially a class containing a set of virtual hooks that a dialect may
-provide a specialization for. In this case, the interface is
-`DialectInlinerInterface`.
+[dialect interface](../../Interfaces.md#dialect-interfaces). This is essentially
+a class containing a set of virtual hooks for which a dialect may provide a
+specialization. In this case, the interface is `DialectInlinerInterface`.
```c++
/// This class defines the interface for handling inlining with Toy operations.
/// This hook is called when a terminator operation has been inlined. The only
/// terminator that we have in the Toy dialect is the return
/// operation(toy.return). We handle the return by replacing the values
- /// previously returned by the call operation, with the operands of the
+ /// previously returned by the call operation with the operands of the
/// return.
void handleTerminator(Operation *op,
ArrayRef<Value *> valuesToRepl) const final {
}
```
-Next, we need to provide a way for the inliner to know that toy.generic_call
+Next, we need to provide a way for the inliner to know that `toy.generic_call`
represents a call to a function. MLIR provides an
[operation interface](../../Interfaces.md#operation-interfaces) that can be used
-to mark an operation as being "call like". Unlike dialect interfaces, operation
+to mark an operation as being "call-like". Unlike dialect interfaces, operation
interfaces provide a more refined granularity of information that is specific
and core to a single operation. The interface that we will be adding here is the
`CallOpInterface`.
To add this interface we just need to include the definition into our operation
-specification file(Ops.td):
+specification file (`Ops.td`):
```.td
#ifdef MLIR_CALLINTERFACES
#endif // MLIR_CALLINTERFACES
```
-and add it to the traits list of GenericCallOp:
+and add it to the traits list of `GenericCallOp`:
```.td
def GenericCallOp : Toy_Op<"generic_call",
```
Now that the inliner has been informed about the Toy dialect, we can add the
-inliner pass to the pass manager for toy:
+inliner pass to the pass manager for Toy:
```c++
pm.addPass(mlir::createInlinerPass());
We have two calls to multiple_transpose that we would like to inline into main,
but if we look at the output nothing has changed. We are missing one last subtle
-piece, there is a hidden type conversion on the edge of the call. If we look at
-the above, the operands to the generic_call are of type `tensor<2x3xf64>` while
+piece: there is a hidden type conversion on the edge of the call. If we look at
+the above, the operands to the generic_call are of type `tensor<2x3xf64>`, while
the inputs to the function expect `tensor<*xf64>`. To resolve this difference,
the inliner expects an explicit cast operation to be inserted. For this, we need
to add a new operation to the Toy dialect, `ToyCastOp`(toy.cast), to represent
Now that we have inlined all of the functions, we are left with a main function
containing a mix of static and dynamically shaped operations. We can now write a
-simple shape inference pass to propagate shapes intra-procedurally within a
-single function. We could write this as a pass that directly encodes the
+simple shape inference pass to propagate shapes intraprocedurally (within a
+single function). We could write this as a pass that directly encodes the
constraints of the operations within the Toy dialect, but this seems like a good
candidate for a transformation that could be written generically. As a good rule
-of thumb, if possible it is better to express a transformation as generically as
-possible so that it could be extended to other dialects in the future. There is
-no telling how many other dialects may have similar needs or encounter the same
+of thumb, it is best to express a transformation as generically as possible,
+such that it can be extended to other dialects in the future. There is no
+telling how many other dialects may have similar needs or encounter the same
problems.
-For shape inference if we break down the problem to its core, we really just
-want operations to tell us what the expected outputs are given a set of
-statically known inputs. We can definitely get more complex than that, but for
-our needs we can keep it simple. Given that this is a property that is core to a
-specific operation, we can define an operation interface that can be specified
-on operations that need to have their result shapes inferred.
+For shape inference, if we break down the problem to its core, we really just
+want operations to tell us the expected outputs given a set of statically known
+inputs. (We can definitely get more complex than that, but for our needs we can
+keep it simple.) Given that this property is core to a specific operation, we
+can define an operation interface that can be specified on operations that need
+to have their result shapes inferred.
Similarly to operations, we can also
[define operation interfaces](../../OpDefinitions.md#operation-interfaces) using
-the operation definition specification(ODS) framework:
+the operation definition specification (ODS) framework.
-The interface is defined by inheriting from `OpInterface`, which takes as a
-template argument the name to be given to the generated c++ interface class. For
-our purposes, we will name the generated class a simpler `ShapeInference`. We
-also provide a description for the interface.
+The interface is defined by inheriting from `OpInterface`, which takes the name
+to be given to the generated C++ interface class as a template argument. For our
+purposes, we will name the generated class a simpler `ShapeInference`. We also
+provide a description for the interface.
```.td
def ShapeInferenceOpInterface : OpInterface<"ShapeInference"> {
}
```
-Next we define the interface methods that the operations will need to provide.
-An interface method is comprised of: a description, a c++ return type in string
-form, a method name in string form, as well as a few optional components
-depending on the need. See the
+Next, we define the interface methods that the operations will need to provide.
+An interface method is comprised of: a description; a C++ return type in string
+form; a method name in string form; and a few optional components, depending on
+the need. See the
[ODS documentation](../../OpDefinitions.md#operation-interfaces) for more
information.
void MulOp::inferShapes() { getResult()->setType(getOperand(0)->getType()); }
```
-At this point, each of the necessary Toy operations provide a mechanism in which
+At this point, each of the necessary Toy operations provide a mechanism by which
to infer their output shapes. The ShapeInferencePass is a FunctionPass: it will
runs on each Function in isolation. MLIR also supports general
[OperationPasses](../../WritingAPass.md#operation-pass) that run on any isolated
-operation(e.g. other function like operations), but here are module only
-contains functions so there is no need to generalize yet.
+operation (i.e. other function-like operations), but here our module only
+contains functions, so there is no need to generalize to all operations.
Implementing such a pass is done by creating a class inheriting from
`mlir::FunctionPass` and overriding the `runOnFunction()` method:
}
```
-You can build `toyc-ch4` and try yourself: `toyc-ch4 test/codegen.toy -emit=mlir
--opt`.
+You can build `toyc-ch4` and try yourself: `toyc-ch4
+test/Examples/Toy/Ch4/codegen.toy -emit=mlir -opt`.
In the [next chapter](Ch-5.md), we will start the process of code generation by
targeting a lower level dialect for optimizing some of the more compute-heavy
-# Chapter 5 - Partial Lowering to Lower-Level Dialects for Optimization
+# Chapter 5: Partial Lowering to Lower-Level Dialects for Optimization
[TOC]
At this point, we are eager to generate actual code and see our Toy language
-taking life. We will obviously use LLVM to generate code, but just showing the
-LLVM builder interface wouldn't be very exciting here. Instead, we will show how
-to perform progressive lowering through a mix of dialects coexisting in the same
-function.
+take life. We will use LLVM to generate code, but just showing the LLVM builder
+interface here wouldn't be very exciting. Instead, we will show how to perform
+progressive lowering through a mix of dialects coexisting in the same function.
-To make it more interesting, in this chapter we will will consider that we want
-to reuse existing optimizations implemented in a dialect optimizing affine
+To make it more interesting, in this chapter we will consider that we want to
+reuse existing optimizations implemented in a dialect optimizing affine
transformations: `Affine`. This dialect is tailored to the computation-heavy
-part of the program, and is limited: it doesn't support representing our
-`toy.print` builtin for instance, neither should it! Instead we can target
+part of the program and is limited: it doesn't support representing our
+`toy.print` builtin, for instance, neither should it! Instead, we can target
`Affine` for the computation heavy part of Toy, and in the
[next chapter](Ch-6.md) directly the `LLVM IR` dialect for lowering `print`. As
part of this lowering, we will be lowering from the
-[TensorType](../../LangRef.md#tensor-type), that `Toy` operates on, to the
+[TensorType](../../LangRef.md#tensor-type) that `Toy` operates on to the
[MemRefType](../../LangRef.md#memref-type) that is indexed via an affine
loop-nest. Tensors represent an abstract value-typed sequence of data, meaning
-that they don't live in any memory. MemRefs on the other hand represent lower
+that they don't live in any memory. MemRefs, on the other hand, represent lower
level buffer access, as they are concrete references to a region of memory.
# Dialect Conversions
-MLIR contains many different dialects, so it is important to have a unified
-framework for converting between them. This is where the `DialectConversion`
-framework comes into play. This framework allows for transforming a set of
-`illegal` operations to a set of `legal` ones. To use this framework we need to
-provide two things:
+MLIR has many different dialects, so it is important to have a unified framework
+for [converting](../../Glossary.md#conversion) between them. This is where the
+`DialectConversion` framework comes into play. This framework allows for
+transforming a set of `illegal` operations to a set of `legal` ones. To use this
+framework, we need to provide two things (and an optional third):
* A [Conversion Target](../../DialectConversion.md#conversion-target)
- - This is the formal specification of what operations, or dialects, are
+ - This is the formal specification of what operations or dialects are
legal for the conversion. Operations that aren't legal will require
- rewrite patterns to perform legalization.
+ rewrite patterns to perform
+ [legalization](./../../Glossary.md#legalization).
* A set of
[Rewrite Patterns](../../DialectConversion.md#rewrite-pattern-specification)
- These are the set of [patterns](../../QuickstartRewrites.md) used to
convert `illegal` operations into a set of zero or more `legal` ones.
-* Optionally, A [Type Converter](../../DialectConversion.md#type-conversion).
+* Optionally, a [Type Converter](../../DialectConversion.md#type-conversion).
- If provided, this is used to convert the types of block arguments. We
won't be needing this for our conversion.
## Conversion Target
-For our purposes, we want to convert the compute intensive `Toy` operations into
+For our purposes, we want to convert the compute-intensive `Toy` operations into
a combination of operations from the `Affine` `Standard` dialects for further
optimization. To start off the lowering, we first define our conversion target:
framework introduced in [chapter 3](Ch-3.md), the
[`DialectConversion` framework](../../DialectConversion.md) also uses
[RewritePatterns](../../QuickstartRewrites.md) to perform the conversion logic.
-These patterns may be the `RewritePatterns` seen before, or a new type of
-pattern specific to the conversion framework `ConversionPattern`.
-`ConversionPatterns` are different from traditional `RewritePatterns` in that
-they accept an additional `operands` parameter containing operands that have
-been remapped/replaced. This is used when dealing with type conversions as the
-pattern will want to operand on values of the new type, but match against the
-old. For our lowering, this invariant will be useful during our lowering as we
-will be translating from the [TensorType](../../LangRef.md#tensor-type),
-currently being operated on, to the [MemRefType](../../LangRef.md#memref-type).
-Let's look at a snippet of lowering the `toy.transpose` operation:
+These patterns may be the `RewritePatterns` seen before or a new type of pattern
+specific to the conversion framework `ConversionPattern`. `ConversionPatterns`
+are different from traditional `RewritePatterns` in that they accept an
+additional `operands` parameter containing operands that have been
+remapped/replaced. This is used when dealing with type conversions, as the
+pattern will want to operate on values of the new type but match against the
+old. For our lowering, this invariant will be useful as it translates from the
+[TensorType](../../LangRef.md#tensor-type) currently being operated on to the
+[MemRefType](../../LangRef.md#memref-type). Let's look at a snippet of lowering
+the `toy.transpose` operation:
```c++
/// Lower the `toy.transpose` operation to an affine loop nest.
## Partial Lowering
Once the patterns have been defined, we can perform the actual lowering. The
-`DialectConversion` framework provides several different modes of lowering, but
-for our purposes we will be performing a partial lowering, as we will not be
-converting `toy.print` at this time.
+`DialectConversion` framework provides several different modes of lowering, but,
+for our purposes, we will perform a partial lowering, as we will not convert
+`toy.print` at this time.
```c++
void ToyToAffineLoweringPass::runOnFunction() {
Before diving into the result of our lowering, this is a good time to discuss
potential design considerations when it comes to partial lowering. In our
-lowering, we will be transforming from a value-type, TensorType, to a
-allocated(buffer-like) type, MemRefType. Given that we will not be lowering the
+lowering, we transform from a value-type, TensorType, to an allocated
+(buffer-like) type, MemRefType. However, given that we do not lower the
`toy.print` operation, we need to temporarily bridge these two worlds. There are
many ways to go about this, each with their own tradeoffs:
Another option would be to have another, lowered, variant of `toy.print` that
operates on the lowered type. The benefit of this option is that there is no
-hidden, unnecessary, copy to optimizer. The downside is that another operation
-definition is needed, that may duplicate many aspects of the first. Defining a
-base class in [ODS](../../OpDefinitions.md) may simplify this, but you still
-need to treat these operations separately.
+hidden, unnecessary copy to the optimizer. The downside is that another
+operation definition is needed that may duplicate many aspects of the first.
+Defining a base class in [ODS](../../OpDefinitions.md) may simplify this, but
+you still need to treat these operations separately.
* Update `toy.print` to allow for operating on the lowered type
## Taking Advantage of Affine Optimization
-Our naive lowering is correct, but it leaves a lot to be desired in regards to
-efficiency; For example the lowering of `toy.mul` has generated some redundant
+Our naive lowering is correct, but it leaves a lot to be desired with regards to
+efficiency. For example, the lowering of `toy.mul` has generated some redundant
loads. Let's look at how adding a few existing optimizations to the pipeline can
help clean this up. Adding the `LoopFusion` and `MemRefDataFlowOpt` passes to
the pipeline gives the following result:
}
```
-Here we can see that an allocation was removed, the two loop nests were fused,
-and we also were able to remove an unnecessary allocation! You can build
-`toyc-ch5` and try yourself: `toyc-ch5 test/lowering.toy -emit=mlir-affine`. We
-can also check our optimizations by adding `-opt`.
+Here, we can see that a redundant allocation was removed, the two loop nests
+were fused, and some unnecessary `load`s were removed. You can build `toyc-ch5`
+and try yourself: `toyc-ch5 test/lowering.toy -emit=mlir-affine`. We can also
+check our optimizations by adding `-opt`.
In this chapter we explored some aspects of partial lowering, with the intent to
optimize. In the [next chapter](Ch-6.md) we will continue the discussion about
-# Chapter 6 - Lowering to LLVM and CodeGeneration
+# Chapter 6: Lowering to LLVM and CodeGeneration
[TOC]
# Lowering to LLVM
For this lowering, we will again use the dialect conversion framework to perform
-the heavy lifting. Although this time, we will be performing a full conversion
-to the [LLVM dialect](../../Dialects/LLVM.md). Thankfully we have already
+the heavy lifting. However, this time, we will be performing a full conversion
+to the [LLVM dialect](../../Dialects/LLVM.md). Thankfully, we have already
lowered all but one of the `toy` operations, with the last being `toy.print`.
Before going over the conversion to LLVM, let's lower the `toy.print` operation.
-We will be lowering `toy.print` to a non-affine loop nest, that invokes `printf`
-for each element. Note that because the dialect conversion framework supports
-transitive lowering, we don't need to directly emit operations in the LLVM
-dialect. By transitive lowering, we mean that the conversion framework may apply
-multiple patterns to fully legalize an operation. In this example, we are
-generating a structured loop nest, instead of the branch-form in the LLVM
-dialect. As long as we have a lowering from the loop operations to LLVM, the
-lowering will still succeed.
+We will lower this operation to a non-affine loop nest that invokes `printf` for
+each element. Note that, because the dialect conversion framework supports
+[transitive lowering](Glossary.md#transitive-lowering), we don't need to
+directly emit operations in the LLVM dialect. By transitive lowering, we mean
+that the conversion framework may apply multiple patterns to fully legalize an
+operation. In this example, we are generating a structured loop nest instead of
+the branch-form in the LLVM dialect. As long as we then have a lowering from the
+loop operations to LLVM, the lowering will still succeed.
During lowering we can get, or build, the declaration for printf as so:
## Type Converter
-During this lowering, we will also be lowering the MemRef types, that are
-currently being operated on, to a representation in LLVM. To perform this
-conversion we use a TypeConverter as part of the lowering. This converter
-details how one type maps to another. This is necessary now that we will be
-doing more complicated lowerings, involving block arguments. Given that we don't
-have any `toy dialect-specific` types that need to be lowered, the default
-converter is enough for our usecase.
+This lowering will also transform the MemRef types which are currently being
+operated on into a representation in LLVM. To perform this conversion, we use a
+TypeConverter as part of the lowering. This converter specifies how one type
+maps to another. This is necessary now that we are performing more complicated
+lowerings involving block arguments. Given that we don't have any
+Toy-dialect-specific types that need to be lowered, the default converter is
+enough for our use case.
```c++
LLVMTypeConverter typeConverter(&getContext());
## Conversion Patterns
Now that the conversion target has been defined, we need to provide the patterns
-used for lowering. At this point of the compilation process, we have a
+used for lowering. At this point in the compilation process, we have a
combination of `toy`, `affine`, and `std` operations. Luckily, the `std` and
-`affine` dialect already provide the set of patterns needed to transform them
+`affine` dialects already provide the set of patterns needed to transform them
into LLVM dialect. These patterns allow for lowering the IR in multiple stages
-by relying on transitive lowering. Transitive lowering, or A->B->C lowering, is
-when multiple patterns must be applied to fully transform an illegal operation
-into a set of legal ones.
+by relying on [transitive lowering](Glossary.md#transitive-lowering).
```c++
mlir::OwningRewritePatternList patterns;
}
```
-We can now lower down to the LLVM dialect:
+We can now lower down to the LLVM dialect, which produces the following code:
```.mlir
llvm.func @free(!llvm<"i8*">)
# CodeGen: Getting Out of MLIR
At this point we are right at the cusp of code generation. We can generate code
-in the LLVM dialect, so now we just need to export to LLVM IR and setup a jit to
+in the LLVM dialect, so now we just need to export to LLVM IR and setup a JIT to
run it.
## Emitting LLVM IR
}
```
-If we enable optimization on the generated LLVM IR, we can trim this down by
-quite a bit:
+If we enable optimization on the generated LLVM IR, we can trim this down quite
+a bit:
```.llvm
define void @main()
3.000000 4.000000
```
-You can also play with -emit=mlir, -emit=mlir-affine, -emit=mlir-llvm, and
--emit=llvm to compare the various levels of IR involved. Also try options like
-[--print-ir-after-all](../../WritingAPass.md#ir-printing) to track the evolution
-of the IR throughout the pipeline.
+You can also play with `-emit=mlir`, `-emit=mlir-affine`, `-emit=mlir-llvm`, and
+`-emit=llvm` to compare the various levels of IR involved. Also try options like
+[`--print-ir-after-all`](../../WritingAPass.md#ir-printing) to track the
+evolution of the IR throughout the pipeline.
So far, we have worked with primitive data types. In the
[next chapter](Ch-7.md), we will add a composite `struct` type.
## Defining a `struct` in Toy
The first thing we need to define is the interface of this type in our `toy`
-source language. The general syntax of a `struct` type in toy is as follows:
+source language. The general syntax of a `struct` type in Toy is as follows:
```toy
# A struct is defined by using the `struct` keyword followed by a name.
#### Reserving a Range of Type Kinds
Types in MLIR rely on having a unique `kind` value to ensure that casting checks
-remain extremely
-efficient([rationale](Rationale.md#reserving-dialect-type-kinds). For `toy`,
-this means we need to explicitly reserve a static range of type `kind` values in
-the symbol registry file
+remain extremely efficient
+([rationale](../../Rationale.md#reserving-dialect-type-kinds)). For `toy`, this
+means we need to explicitly reserve a static range of type `kind` values in the
+symbol registry file
[DialectSymbolRegistry](https://github.com/tensorflow/mlir/blob/master/include/mlir/IR/DialectSymbolRegistry.def).
```c++
instance of an `MLIRContext`. When constructing a `Type`, we are internally just
constructing and uniquing an instance of a storage class.
-When defining a new `Type` that requires additional information than just the
-`kind`, like our struct type for the element types, we will need to provide a
-derived storage class. The `primitive` types that don't have any additional
-data, like the [`index` type](../../LangRef.md#index-type), don't require a
-storage class.
+When defining a new `Type` that requires additional information beyond just the
+`kind` (e.g. the `struct` type, which requires additional information to hold
+the element types), we will need to provide a derived storage class. The
+`primitive` types that don't have any additional data (e.g. the
+[`index` type](../../LangRef.md#index-type)) don't require a storage class.
##### Defining the Storage Class
##### Defining the Type Class
-With the storage class defined, we can add the definition for the user visible
+With the storage class defined, we can add the definition for the user-visible
`StructType` class. This is the class that we will actually interface with.
```c++
};
```
-and we register this type in the `ToyDialect` constructor in a similar way to
-how we did with operations:
+We register this type in the `ToyDialect` constructor in a similar way to how we
+did with operations:
```c++
ToyDialect::ToyDialect(mlir::MLIRContext *ctx)
```
With this we can now use our `StructType` when generating MLIR from Toy. See
-MLIRGen.cpp for more details.
+examples/toy/Ch7/mlir/MLIRGen.cpp for more details.
### Parsing and Printing
At this point we can use our `StructType` during MLIR generation and
-transformation, but we can't output or parse `.mlir`. To support this we need to
-add support for parsing and printing instances of the `StructType`. This support
-can be added by overriding the `parseType` and `printType` methods on the
-`ToyDialect`.
+transformation, but we can't output or parse `.mlir`. For this we need to add
+support for parsing and printing instances of the `StructType`. This can be done
+by overriding the `parseType` and `printType` methods on the `ToyDialect`.
```c++
class ToyDialect : public mlir::Dialect {
};
```
-These methods take an instance of a high level parser or printer that allows for
+These methods take an instance of a high-level parser or printer that allows for
easily implementing the necessary functionality. Before going into the
implementation, let's think about the syntax that we want for the `struct` type
in the printed IR. As described in the
[MLIR language reference](../../LangRef.md#dialect-types), dialect types are
-generally represented as: `!` dialect-namespace `<` type-data `>`; With a pretty
+generally represented as: `! dialect-namespace < type-data >`, with a pretty
form available under certain circumstances. The responsibility of our `Toy`
parser and printer is to provide the `type-data` bits. We will define our
`StructType` as having the following form:
#### Printing
-As implementation of the printer is shown below:
+An implementation of the printer is shown below:
```c++
/// Print an instance of a type registered to the toy dialect.
### Operating on `StructType`
-Now that the `struct` type has been defined, and we can roundtrip it through the
-IR. The next step is to add support for using it within our operations.
+Now that the `struct` type has been defined, and we can round-trip it through
+the IR. The next step is to add support for using it within our operations.
#### Updating Existing Operations
A few of our existing operations will need to be updated to handle `StructType`.
-The first step is to make the ODS framework aware of our Type, so that we can
-use it in the operation definitions. A simple example is shown below:
+The first step is to make the ODS framework aware of our Type so that we can use
+it in the operation definitions. A simple example is shown below:
```td
// Provide a definition for the Toy StructType for use in ODS. This allows for
def Toy_Type : AnyTypeOf<[F64Tensor, Toy_StructType]>;
```
-We can then update our operations, like `ReturnOp` for example, to also accept
-the `Toy_StructType`:
+We can then update our operations, e.g. `ReturnOp`, to also accept the
+`Toy_StructType`:
```td
def ReturnOp : Toy_Op<"return", [Terminator, HasParent<"FuncOp">]> {
##### `toy.struct_constant`
This new operation materializes a constant value for a struct. In our current
-modeling we just use an [array attribute](../../LangRef.md#array-attribute) that
-contains a set of constant values for each of the `struct` elements.
+modeling, we just use an [array attribute](../../LangRef.md#array-attribute)
+that contains a set of constant values for each of the `struct` elements.
```mlir
%0 = "toy.struct_constant"() {
#### Optimizing Operations on `StructType`
Now that we have a few operations operating on `StructType`, we also have many
-new constant folding opportunities. After inlining the MLIR module in the
-previous section looks something like:
+new constant folding opportunities.
+
+After inlining, the MLIR module in the previous section looks something like:
```mlir
module {
}
```
-With this we can now generate code that can be generated to LLVM without any
+With this, we can now generate code that can be generated to LLVM without any
changes to our pipeline.
```mlir
}
```
-You can build `toyc-ch7` and try yourself: `toyc-ch7 test/struct-codegen.toy
--emit=mlir`. More details can on defining custom types can be found in
+You can build `toyc-ch7` and try yourself: `toyc-ch7
+test/Examples/Toy/Ch7/struct-codegen.toy -emit=mlir`. More details on defining
+custom types can be found in
[DefiningAttributesAndTypes](../../DefiningAttributesAndTypes.md).
work on instances of operations at different levels of nesting. The structure of
the [pass manager](#pass-manager), and the concept of nesting, is detailed
further below. All passes in MLIR derive from `OperationPass` and adhere to the
-following restrictions; Any noncompliance will lead to problematic behavior in
+following restrictions; any noncompliance will lead to problematic behavior in
multithreaded and other advanced scenarios:
* Modify anything within the parent block/region/operation/etc, outside of the
current operation being operated on. This includes adding or removing
operations from the parent block.
-* Maintain pass state across invocations of runOnOperation. A pass may be run
- on several different operations with no guarantee of execution order.
+* Maintain pass state across invocations of `runOnOperation`. A pass may be
+ run on several different operations with no guarantee of execution order.
* When multithreading, a specific pass instance may not even execute on
all operations within the module. As such, a pass should not rely on
running on all operations.