using llvm::SmallVector;
using llvm::StringRef;
using llvm::Twine;
-using std::make_unique;
namespace {
/// At this point we take advantage of the "raw" MLIR APIs to create operations
/// that haven't been registered in any way with MLIR. These operations are
/// unknown to MLIR, custom passes could operate by string-matching the name of
-/// these operations, but no other type checking or semantic is associated with
-/// them natively by MLIR.
+/// these operations, but no other type checking or semantics are associated
+/// with them natively by MLIR.
class MLIRGenImpl {
public:
- MLIRGenImpl(mlir::MLIRContext &context) : context(context) {}
+ MLIRGenImpl(mlir::MLIRContext &context)
+ : context(context), builder(&context) {}
/// Public API: convert the AST for a Toy module (source file) to an MLIR
- /// Module.
+ /// Module operation.
mlir::ModuleOp mlirGen(ModuleAST &moduleAST) {
// We create an empty MLIR module and codegen functions one at a time and
// add them to the module.
// FIXME: (in the next chapter...) without registering a dialect in MLIR,
// this won't do much, but it should at least check some structural
- // properties.
+ // properties of the generated MLIR module.
if (failed(mlir::verify(theModule))) {
- emitError(mlir::UnknownLoc::get(&context), "Module verification error");
+ theModule.emitError("Module verification error");
return nullptr;
}
private:
/// In MLIR (like in LLVM) a "context" object holds the memory allocation and
- /// the ownership of many internal structure of the IR and provide a level
- /// of "uniquing" across multiple modules (types for instance).
+ /// ownership of many internal structures of the IR and provides a level of
+ /// "uniquing" across multiple modules (types for instance).
mlir::MLIRContext &context;
- /// A "module" matches a source file: it contains a list of functions.
+ /// A "module" matches a Toy source file: containing a list of functions.
mlir::ModuleOp theModule;
- /// The builder is a helper class to create IR inside a function. It is
- /// re-initialized every time we enter a function and kept around as a
- /// convenience for emitting individual operations.
- /// The builder is stateful, in particular it keeeps an "insertion point":
- /// this is where the next operations will be introduced.
- std::unique_ptr<mlir::OpBuilder> builder;
+ /// The builder is a helper class to create IR inside a function. The builder
+ /// is stateful, in particular it keeeps an "insertion point": this is where
+ /// the next operations will be introduced.
+ mlir::OpBuilder builder;
/// The symbol table maps a variable name to a value in the current scope.
/// Entering a function creates a new scope, and the function arguments are
/// Helper conversion for a Toy AST location to an MLIR location.
mlir::Location loc(Location loc) {
- return mlir::FileLineColLoc::get(mlir::Identifier::get(*loc.file, &context),
- loc.line, loc.col, &context);
+ return builder.getFileLineColLoc(builder.getIdentifier(*loc.file), loc.line,
+ loc.col);
}
- /// Declare a variable in the current scope, return true if the variable
+ /// Declare a variable in the current scope, return success if the variable
/// wasn't declared yet.
- bool declare(llvm::StringRef var, mlir::Value *value) {
- if (symbolTable.count(var)) {
- return false;
- }
+ mlir::LogicalResult declare(llvm::StringRef var, mlir::Value *value) {
+ if (symbolTable.count(var))
+ return mlir::failure();
symbolTable.insert(var, value);
- return true;
+ return mlir::success();
}
/// Create the prototype for an MLIR function with as many arguments as the
// Arguments type is uniformly a generic array.
llvm::SmallVector<mlir::Type, 4> arg_types(proto.getArgs().size(),
getType(VarType{}));
- auto func_type = mlir::FunctionType::get(arg_types, ret_types, &context);
+ auto func_type = builder.getFunctionType(arg_types, ret_types);
auto function = mlir::FuncOp::create(loc(proto.loc()), proto.getName(),
func_type, /* attrs = */ {});
// Mark the function as generic: it'll require type specialization for every
// call site.
if (function.getNumArguments())
- function.setAttr("toy.generic", mlir::BoolAttr::get(true, &context));
-
+ function.setAttr("toy.generic", builder.getUnitAttr());
return function;
}
// argument list as the function itself.
auto &entryBlock = *function.addEntryBlock();
auto &protoArgs = funcAST.getProto()->getArgs();
+
// Declare all the function arguments in the symbol table.
for (const auto &name_value :
llvm::zip(protoArgs, entryBlock.getArguments())) {
- declare(std::get<0>(name_value)->getName(), std::get<1>(name_value));
+ if (failed(declare(std::get<0>(name_value)->getName(),
+ std::get<1>(name_value))))
+ return nullptr;
}
- // Create a builder for the function, it will be used throughout the codegen
- // to create operations in this function.
- builder = std::make_unique<mlir::OpBuilder>(function.getBody());
+ // Set the insertion point in the builder to the beginning of the function
+ // body, it will be used throughout the codegen to create operations in this
+ // function.
+ builder.setInsertionPointToStart(&entryBlock);
// Emit the body of the function.
- if (!mlirGen(*funcAST.getBody())) {
+ if (mlir::failed(mlirGen(*funcAST.getBody()))) {
function.erase();
return nullptr;
}
// Implicitly return void if no return statement was emitted.
// FIXME: we may fix the parser instead to always return the last expression
// (this would possibly help the REPL case later)
- if (function.getBlocks().back().back().getName().getStringRef() !=
+ if (function.getBody().back().back().getName().getStringRef() !=
"toy.return") {
ReturnExprAST fakeRet(funcAST.getProto()->loc(), llvm::None);
mlirGen(fakeRet);
op_name = "toy.mul";
break;
default:
- emitError(loc(binop.loc()), "Error: invalid binary operator '")
+ emitError(location, "Error: invalid binary operator '")
<< binop.getOp() << "'";
return nullptr;
}
// Build the MLIR operation from the name and the two operands. The return
// type is always a generic array for binary operators.
mlir::OperationState result(location, op_name);
- result.types.push_back(getType(VarType{}));
- result.operands.push_back(L);
- result.operands.push_back(R);
- return builder->createOperation(result)->getResult(0);
+ result.addTypes(getType(VarType{}));
+ result.addOperands({L, R});
+ return builder.createOperation(result)->getResult(0);
}
- // This is a reference to a variable in an expression. The variable is
- // expected to have been declared and so should have a value in the symbol
- // table, otherwise emit an error and return nullptr.
+ /// This is a reference to a variable in an expression. The variable is
+ /// expected to have been declared and so should have a value in the symbol
+ /// table, otherwise emit an error and return nullptr.
mlir::Value *mlirGen(VariableExprAST &expr) {
- if (symbolTable.count(expr.getName()))
- return symbolTable.lookup(expr.getName());
+ if (auto *variable = symbolTable.lookup(expr.getName()))
+ return variable;
+
emitError(loc(expr.loc()), "Error: unknown variable '")
<< expr.getName() << "'";
return nullptr;
}
- // Emit a return operation, return true on success.
- bool mlirGen(ReturnExprAST &ret) {
- auto location = loc(ret.loc());
+ /// Emit a return operation. This will return failure if any generation fails.
+ mlir::LogicalResult mlirGen(ReturnExprAST &ret) {
+ mlir::OperationState result(loc(ret.loc()), "toy.return");
+
// `return` takes an optional expression, we need to account for it here.
- mlir::OperationState result(location, "toy.return");
if (ret.getExpr().hasValue()) {
auto *expr = mlirGen(*ret.getExpr().getValue());
if (!expr)
- return false;
- result.operands.push_back(expr);
+ return mlir::failure();
+ result.addOperands(expr);
}
- builder->createOperation(result);
- return true;
+
+ builder.createOperation(result);
+ return mlir::success();
}
- // Emit a literal/constant array. It will be emitted as a flattened array of
- // data in an Attribute attached to a `toy.constant` operation.
- // See documentation on [Attributes](LangRef.md#attributes) for more details.
- // Here is an excerpt:
- //
- // Attributes are the mechanism for specifying constant data in MLIR in
- // places where a variable is never allowed [...]. They consist of a name
- // and a [concrete attribute value](#attribute-values). It is possible to
- // attach attributes to operations, functions, and function arguments. The
- // set of expected attributes, their structure, and their interpretation
- // are all contextually dependent on what they are attached to.
- //
- // Example, the source level statement:
- // var a<2, 3> = [[1, 2, 3], [4, 5, 6]];
- // will be converted to:
- // %0 = "toy.constant"() {value: dense<tensor<2x3xf64>,
- // [[1.000000e+00, 2.000000e+00, 3.000000e+00],
- // [4.000000e+00, 5.000000e+00, 6.000000e+00]]>} : () -> memref<2x3xf64>
- //
+ /// Emit a literal/constant array. It will be emitted as a flattened array of
+ /// data in an Attribute attached to a `toy.constant` operation.
+ /// See documentation on [Attributes](LangRef.md#attributes) for more details.
+ /// Here is an excerpt:
+ ///
+ /// Attributes are the mechanism for specifying constant data in MLIR in
+ /// places where a variable is never allowed [...]. They consist of a name
+ /// and a concrete attribute value. The set of expected attributes, their
+ /// structure, and their interpretation are all contextually dependent on
+ /// what they are attached to.
+ ///
+ /// Example, the source level statement:
+ /// var a<2, 3> = [[1, 2, 3], [4, 5, 6]];
+ /// will be converted to:
+ /// %0 = "toy.constant"() {value: dense<tensor<2x3xf64>,
+ /// [[1.000000e+00, 2.000000e+00, 3.000000e+00],
+ /// [4.000000e+00, 5.000000e+00, 6.000000e+00]]>} : () -> tensor<2x3xf64>
+ ///
mlir::Value *mlirGen(LiteralExprAST &lit) {
- auto location = loc(lit.loc());
auto type = getType(lit.getDims());
- // The attribute is a vector with an attribute per element (number) in the
- // array, see `collectData()` below for more details.
- std::vector<mlir::Attribute> data;
+ // The attribute is a vector with a floating point value per element
+ // (number) in the array, see `collectData()` below for more details.
+ std::vector<double> data;
data.reserve(std::accumulate(lit.getDims().begin(), lit.getDims().end(), 1,
std::multiplies<int>()));
collectData(lit, data);
- // FIXME: using a tensor type is a HACK here.
- // Can we do differently without registering a dialect? Using a string blob?
- mlir::Type elementType = mlir::FloatType::getF64(&context);
- auto dataType = builder->getTensorType(lit.getDims(), elementType);
+ // The type of this attribute is tensor of 64-bit floating-point with the
+ // shape of the literal.
+ mlir::Type elementType = builder.getF64Type();
+ auto dataType = builder.getTensorType(lit.getDims(), elementType);
- // This is the actual attribute that actually hold the list of values for
- // this array literal.
- auto dataAttribute = builder->getNamedAttr(
- "value", builder->getDenseElementsAttr(dataType, data)
- .cast<mlir::DenseElementsAttr>());
+ // This is the actual attribute that holds the list of values for this
+ // tensor literal.
+ auto dataAttribute =
+ mlir::DenseElementsAttr::get(dataType, llvm::makeArrayRef(data));
// Build the MLIR op `toy.constant`, only boilerplate below.
- mlir::OperationState result(location, "toy.constant");
- result.types.push_back(type);
- result.attributes.push_back(dataAttribute);
- return builder->createOperation(result)->getResult(0);
+ mlir::OperationState result(loc(lit.loc()), "toy.constant");
+ result.addTypes(type);
+ result.addAttribute("value", dataAttribute);
+ return builder.createOperation(result)->getResult(0);
}
- // Recursive helper function to accumulate the data that compose an array
- // literal. It flattens the nested structure in the supplied vector. For
- // example with this array:
- // [[1, 2], [3, 4]]
- // we will generate:
- // [ 1, 2, 3, 4 ]
- // Individual numbers are wrapped in a light wrapper `mlir::FloatAttr`.
- // Attributes are the way MLIR attaches constant to operations and functions.
- void collectData(ExprAST &expr, std::vector<mlir::Attribute> &data) {
+ /// Recursive helper function to accumulate the data that compose an array
+ /// literal. It flattens the nested structure in the supplied vector. For
+ /// example with this array:
+ /// [[1, 2], [3, 4]]
+ /// we will generate:
+ /// [ 1, 2, 3, 4 ]
+ /// Individual numbers are represented as doubles.
+ /// Attributes are the way MLIR attaches constant to operations.
+ void collectData(ExprAST &expr, std::vector<double> &data) {
if (auto *lit = dyn_cast<LiteralExprAST>(&expr)) {
for (auto &value : lit->getValues())
collectData(*value, data);
return;
}
+
assert(isa<NumberExprAST>(expr) && "expected literal or number expr");
- mlir::Type elementType = mlir::FloatType::getF64(&context);
- auto attr = mlir::FloatAttr::getChecked(
- elementType, cast<NumberExprAST>(expr).getValue(), loc(expr.loc()));
- data.push_back(attr);
+ data.push_back(cast<NumberExprAST>(expr).getValue());
}
- // Emit a call expression. It emits specific operations for the `transpose`
- // builtin. Other identifiers are assumed to be user-defined functions.
+ /// Emit a call expression. It emits specific operations for the `transpose`
+ /// builtin. Other identifiers are assumed to be user-defined functions.
mlir::Value *mlirGen(CallExprAST &call) {
- auto location = loc(call.loc());
- std::string callee = call.getCallee();
+ llvm::StringRef callee = call.getCallee();
+
// Codegen the operands first.
SmallVector<mlir::Value *, 4> operands;
for (auto &expr : call.getArgs()) {
return nullptr;
operands.push_back(arg);
}
- // builtin have their custom operation, this is a straightforward emission.
+
+ // Builting calls have their custom operation, meaning this is a
+ // straightforward emission.
if (callee == "transpose") {
- mlir::OperationState result(location, "toy.transpose");
- result.types.push_back(getType(VarType{}));
+ mlir::OperationState result(loc(call.loc()), "toy.transpose");
+ result.addTypes(getType(VarType{}));
result.operands = std::move(operands);
- return builder->createOperation(result)->getResult(0);
+ return builder.createOperation(result)->getResult(0);
}
- // Calls to user-defined functions are mapped to a custom call that takes
- // the callee name as an attribute.
- mlir::OperationState result(location, "toy.generic_call");
- result.types.push_back(getType(VarType{}));
+ // Otherwise this is a call to a user-defined function. Calls to
+ // user-defined functions are mapped to a custom call that takes the callee
+ // name as an attribute.
+ mlir::OperationState result(loc(call.loc()), "toy.generic_call");
+ result.addTypes(getType(VarType{}));
result.operands = std::move(operands);
- auto calleeAttr = builder->getStringAttr(call.getCallee());
- result.attributes.push_back(builder->getNamedAttr("callee", calleeAttr));
- return builder->createOperation(result)->getResult(0);
+ result.addAttribute("callee", builder.getSymbolRefAttr(callee));
+ return builder.createOperation(result)->getResult(0);
}
- // Emit a call expression. It emits specific operations for two builtins:
- // transpose(x) and print(x). Other identifiers are assumed to be user-defined
- // functions. Return false on failure.
- bool mlirGen(PrintExprAST &call) {
+ /// Emit a print expression. It emits specific operations for two builtins:
+ /// transpose(x) and print(x).
+ mlir::LogicalResult mlirGen(PrintExprAST &call) {
auto *arg = mlirGen(*call.getArg());
if (!arg)
- return false;
- auto location = loc(call.loc());
- mlir::OperationState result(location, "toy.print");
- result.operands.push_back(arg);
- builder->createOperation(result);
- return true;
+ return mlir::failure();
+
+ mlir::OperationState result(loc(call.loc()), "toy.print");
+ result.addOperands(arg);
+ builder.createOperation(result);
+ return mlir::success();
}
- // Emit a constant for a single number (FIXME: semantic? broadcast?)
+ /// Emit a constant for a single number (FIXME: semantic? broadcast?)
mlir::Value *mlirGen(NumberExprAST &num) {
- auto location = loc(num.loc());
- mlir::OperationState result(location, "toy.constant");
- mlir::Type elementType = mlir::FloatType::getF64(&context);
- result.types.push_back(builder->getMemRefType({1}, elementType));
- auto attr = mlir::FloatAttr::getChecked(elementType, num.getValue(),
- loc(num.loc()));
- result.attributes.push_back(builder->getNamedAttr("value", attr));
- return builder->createOperation(result)->getResult(0);
+ mlir::OperationState result(loc(num.loc()), "toy.constant");
+ mlir::Type elementType = builder.getF64Type();
+ result.addTypes(builder.getTensorType({}, elementType));
+ result.addAttribute("value", builder.getF64FloatAttr(num.getValue()));
+ return builder.createOperation(result)->getResult(0);
}
- // Dispatch codegen for the right expression subclass using RTTI.
+ /// Dispatch codegen for the right expression subclass using RTTI.
mlir::Value *mlirGen(ExprAST &expr) {
switch (expr.getKind()) {
case toy::ExprAST::Expr_BinOp:
}
}
- // Handle a variable declaration, we'll codegen the expression that forms the
- // initializer and record the value in the symbol table before returning it.
- // Future expressions will be able to reference this variable through symbol
- // table lookup.
+ /// Handle a variable declaration, we'll codegen the expression that forms the
+ /// initializer and record the value in the symbol table before returning it.
+ /// Future expressions will be able to reference this variable through symbol
+ /// table lookup.
mlir::Value *mlirGen(VarDeclExprAST &vardecl) {
- mlir::Value *value = nullptr;
- auto location = loc(vardecl.loc());
- if (auto init = vardecl.getInitVal()) {
- value = mlirGen(*init);
- if (!value)
- return nullptr;
- // We have the initializer value, but in case the variable was declared
- // with specific shape, we emit a "reshape" operation. It will get
- // optimized out later as needed.
- if (!vardecl.getType().shape.empty()) {
- mlir::OperationState result(location, "toy.reshape");
- result.types.push_back(getType(vardecl.getType()));
- result.operands.push_back(value);
- value = builder->createOperation(result)->getResult(0);
- }
- } else {
+ auto init = vardecl.getInitVal();
+ if (!init) {
emitError(loc(vardecl.loc()),
"Missing initializer in variable declaration");
return nullptr;
}
+
+ mlir::Value *value = mlirGen(*init);
+ if (!value)
+ return nullptr;
+
+ // We have the initializer value, but in case the variable was declared
+ // with specific shape, we emit a "reshape" operation. It will get
+ // optimized out later as needed.
+ if (!vardecl.getType().shape.empty()) {
+ mlir::OperationState result(loc(vardecl.loc()), "toy.reshape");
+ result.addTypes(getType(vardecl.getType()));
+ result.addOperands(value);
+ value = builder.createOperation(result)->getResult(0);
+ }
+
// Register the value in the symbol table
- declare(vardecl.getName(), value);
+ if (failed(declare(vardecl.getName(), value)))
+ return nullptr;
return value;
}
- /// Codegen a list of expression, return false if one of them hit an error.
- bool mlirGen(ExprASTList &blockAST) {
+ /// Codegen a list of expression, return failure if one of them hit an error.
+ mlir::LogicalResult mlirGen(ExprASTList &blockAST) {
ScopedHashTableScope<llvm::StringRef, mlir::Value *> var_scope(symbolTable);
for (auto &expr : blockAST) {
// Specific handling for variable declarations, return statement, and
// expressions.
if (auto *vardecl = dyn_cast<VarDeclExprAST>(expr.get())) {
if (!mlirGen(*vardecl))
- return false;
+ return mlir::failure();
continue;
}
- if (auto *ret = dyn_cast<ReturnExprAST>(expr.get())) {
- if (!mlirGen(*ret))
- return false;
- return true;
- }
+ if (auto *ret = dyn_cast<ReturnExprAST>(expr.get()))
+ return mlirGen(*ret);
if (auto *print = dyn_cast<PrintExprAST>(expr.get())) {
- if (!mlirGen(*print))
- return false;
+ if (mlir::failed(mlirGen(*print)))
+ return mlir::success();
continue;
}
+
// Generic expression dispatch codegen.
if (!mlirGen(*expr))
- return false;
+ return mlir::failure();
}
- return true;
+ return mlir::success();
}
- /// Build a type from a list of shape dimensions. Types are `array` followed
- /// by an optional dimension list, example: array<2, 2>
- /// They are wrapped in a `toy` dialect (see next chapter) and get printed:
- /// !toy.array<2, 2>
- template <typename T> mlir::Type getType(T shape) {
- std::string typeName = "array";
- if (!shape.empty()) {
- typeName += "<";
- const char *sep = "";
- for (auto dim : shape) {
- typeName += sep;
- typeName += llvm::Twine(dim).str();
- sep = ", ";
- }
- typeName += ">";
- }
- return mlir::OpaqueType::get(mlir::Identifier::get("toy", &context),
- typeName, &context);
+ /// Build a tensor type from a list of shape dimensions.
+ mlir::Type getType(llvm::ArrayRef<int64_t> shape) {
+ // If the shape is empty, then this type is unranked.
+ if (shape.empty())
+ return builder.getTensorType(builder.getF64Type());
+
+ // Otherwise, we use the given shape.
+ return builder.getTensorType(shape, builder.getF64Type());
}
/// Build an MLIR type from a Toy AST variable type
## Introduction: Multi-Level IR
Other compilers like LLVM (see the
-[Kaleidoscope tutorial](https://llvm.org/docs/tutorial/LangImpl01.html)) offer
-a fixed set of predefined types and, usually *low-level* / RISC-like,
+[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
-transformation like C++ template instantiation through AST cloning and rewrite.
-Finally, languages with construction higher-level than C/C++ may require
+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.
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 is
-little to no pre-defined set of instructions (*operations* in MLIR
-terminology) or types.
+addresses this issue by being designed for extensibility. As such, there are
+little to no pre-defined instructions (*operations* in MLIR terminology) or
+types.
-## MLIR Module, Functions, Blocks, and Operations
+## MLIR Dialects and Operations
-[Language reference](../../LangRef.md#operations)
+[Language reference](../../LangRef.md#dialects)
-In MLIR (like in LLVM), the top level structure for the IR is a Module
-(equivalent to a translation unit in C/C++). A module contains a list of
-functions, and each function has a list of blocks forming a CFG. Each block is a
-list of operations that execute in sequence.
+In MLIR, the core unit of abstraction and computation is an `Operation`, similar
+in many ways to LLVM instructions. Operations can be used to represent all of
+the core IR structures in LLVM: instructions, globals(like functions), modules,
+etc; however MLIR does not have a closed set of operations. Instead, the MLIR
+operation set is fully extensible and operations can have application-specific
+semantics.
-Operations in MLIR are similar to instructions in LLVM, however MLIR does not
-have a closed set of operations. Instead, MLIR operations are fully extensible
-and can have application-specific semantics.
+MLIR supports this extensibility with the concept of
+[Dialects](../../LangRef.md#dialects). Among other things, Dialects provide a
+grouping mechanism for operations under a unique `namespace`. Dialects will be a
+discussed a bit more in the [next chapter](Ch-3.md).
Here is the MLIR assembly for the Toy 'transpose' operations:
```MLIR(.mlir)
-%t_array = "toy.transpose"(%array) { inplace: true } : (!toy.array<2, 3>) -> !toy.array<3, 2>
+%t_tensor = "toy.transpose"(%tensor) { inplace = true } : (tensor<2x3xf64>) -> tensor<3x2xf64>
```
Let's look at the anatomy of this MLIR operation:
- it is identified by its name, which is expected to be a unique string (e.g.
`toy.transpose`).
+ * the operation name is split in two parts: the dialect namespace prefix,
+ and the specific op name. This can be read as the `transpose` operation
+ in the `toy` dialect.
- it takes as input zero or more operands (or arguments), which are SSA values
- defined by other operations or referring to function and block arguments
- (e.g. `%array`).
-- it produces zero or more results (we will limit ourselves to a single result
- in the context of Toy), which are SSA values (e.g. `%t_array`).
+ defined by other operations or referring to block arguments (e.g.
+ `%tensor`).
+- it produces zero or more results (we will limit ourselves to single result
+ operations in the context of Toy), which are SSA values (e.g. `%t_tensor`).
- it has zero or more attributes, which are special operands that are always
- constant (e.g. `inplace: true`).
-- Lastly the type of the operation appears at the end in a functional form,
+ constant (e.g. `inplace = true`).
+- lastly, the type of the operation appears at the end in a functional form,
spelling the types of the arguments in parentheses and the type of the
return values afterward.
operations requiring it. Dropping a location becomes an explicit choice and
cannot happen by mistake.
+## Opaque API
-## Opaque Builder API
-
-Operations and types can be created with only their string names using the
-raw builder API. This allows MLIR to parse, represent, and round-trip any valid
-IR. For example, the following can round-trip through *mlir-opt*:
+MLIR is designed to be a completely extensible system, as such the
+infrastructure has the capability to opaquely represent operations (as well as
+attributes, types, etc.) that have not been registered. This allows MLIR to
+parse, represent, and round-trip any valid IR. For example, the following can
+round-trip through *mlir-opt*:
```MLIR(.mlir)
func @some_func(%arg0: !random_dialect<"custom_type">) -> !another_dialect<"other_type"> {
- %result = "custom.operation"(%arg0) : (!random_dialect<"custom_type">) -> !another_dialect<"other_type">
+ %result = "custom.operation"(%arg0) { attr = #random_dialect<"custom_attribute"> } : (!random_dialect<"custom_type">) -> !another_dialect<"other_type">
return %result : !another_dialect<"other_type">
}
```
Here MLIR will enforce some structural constraints (SSA, block termination,
-return operand type coherent with function return type, etc.) but otherwise the
-types and the operation are completely opaque.
+etc.) but otherwise the types and the `custom.operation` are completely opaque.
-We will take advantage of this facility to emit MLIR for Toy by traversing the
-AST. Our types will be prefixed with "!toy" and our operation name with "toy.".
-MLIR refers to this prefix as a *dialect*, we will introduce this with more
-details in the [next chapter](Ch-3.md).
+We will take advantage of this facility for the initial emission of MLIR for Toy
+by traversing the AST. Our operation names will be prefixed `toy.` in
+preparation for a `toy` dialect, which we will introduce with more details in
+the [next chapter](Ch-3.md).
-Programmatically creating an opaque operation like the one above involves using
-the `mlir::OperationState` structure which group all the basic elements needs to
-build an operation with an `mlir::Builder`:
+Programmatically creating an opaque operation, like the one above, involves
+using the `mlir::OperationState` structure which group all the basic elements
+needed to build an operation with an `mlir::OpBuilder`:
- The name of the operation.
-- A location for debugging purpose. It is mandatory, but can be explicitly set
- to "unknown".
-- The list of operand values.
-- The types for returned values.
-- The list of attributes.
-- A list of successors (for branches mostly).
+- A location for debugging purposes. It is mandatory, but can be explicitly
+ set to `unknown`.
+- A list of operand values.
+- A list of types for result values.
+- A list of attributes.
+- A list of successors blocks (for branches mostly).
+- A list of regions (for structural operations like functions).
To build the `custom.operation` from the listing above, assuming you have a
`Value *` handle to `%arg0`, is as simple as:
```c++
+// Creation of the state defining the operation:
+mlir::OperationState state(location, "custom.operation");
+state.addOperands(arg0);
+
// The return type for the operation: `!another_dialect<"other_type">`
-auto another_dialect_prefix = mlir::Identifier::get("another_dialect", &context);
+auto anotherDialectPrefix = mlir::Identifier::get("another_dialect", &context);
auto returnType = mlir::OpaqueType::get(another_dialect_prefix,
- "custom_type", &context);
-// Creation of the state defining the operation:
-mlir::OperationState state(&context, location, "custom.operation");
-state.types.push_back(returnType);
-state.operands.push_back(arg0);
+ "custom_type", &context);
+state.addTypes(returnType);
+
+
// Using a builder to create the operation and insert it where the builder
// insertion point is currently set.
-auto customOperation = builder->createOperation(state);
+Operation *customOperation = builder.createOperation(state);
+
// An operation is not an SSA value (unlike LLVM), because it can return
-// multiple SSA value, the resulting value can be obtained:
+// multiple SSA values, the resulting value can be obtained:
Value *result = customOperation->getResult(0);
```
generation through a simple depth-first search traversal of the Toy AST. Here is
how we create a `toy.transpose` operation:
-```
+```c++
mlir::Operation *createTransposeOp(OpBuilder &builder,
- mlir::Value *input_array) {
- // We bundle our custom type in a `toy` dialect.
- auto toyDialect = mlir::Identifier::get("toy", builder->getContext());
- // Create a custom type, in the MLIR assembly it is: !toy.array<2, 2>
- auto type = mlir::OpaqueType::get(toyDialect, "array<2, 2>", builder->getContext());
-
- // Fill the `OperationState` with the required fields
- mlir::OperationState result(builder->getContext(), location, "toy.transpose");
- result.types.push_back(type); // return type
- result.operands.push_back(input_value); // argument
+ mlir::Value *input_tensor) {
+ // Fill the `OperationState` with the required fields.
+ mlir::OperationState result(location, "toy.transpose");
+ result.addOperands(input_tensor);
+
+ // We use the MLIR tensor type for 'toy' types.
+ auto type = builder.getTensorType({2, 2}, builder.getF64Type());
+ result.addTypes(type);
+
+ // Create the transpose operation.
Operation *newTransposeOp = builder->createOperation(result);
return newTransposeOp;
}
## Complete Toy Example
-FIXME: It would be nice to have an idea for the **need** of a custom **type** in
-Toy? Right now `toy<array>` could be replaced directly by unranked `tensor<*>`
-and `toy<array<YxZ>>` could be replaced by a `memref<YxZ>`.
-
At this point we can already generate our "Toy IR" without having registered
anything with MLIR. A simplified version of the previous example:
Results in the following IR:
```MLIR(.mlir)
-func @multiply_transpose(%arg0: !toy<"array">, %arg1: !toy<"array">)
- attributes {toy.generic: true} loc("test/codegen.toy":2:1) {
- %0 = "toy.transpose"(%arg1) : (!toy<"array">) -> !toy<"array"> loc("test/codegen.toy":3:14)
- %1 = "toy.mul"(%arg0, %0) : (!toy<"array">, !toy<"array">) -> !toy<"array"> loc("test/codegen.toy":3:14)
- "toy.return"(%1) : (!toy<"array">) -> () loc("test/codegen.toy":3:3)
-}
-
-func @main() loc("test/codegen.toy":6:1) {
- %0 = "toy.constant"() {value: dense<tensor<2x3xf64>, [[1.000000e+00, 2.000000e+00, 3.000000e+00], [4.000000e+00, 5.000000e+00, 6.000000e+00]]>} : () -> !toy.array<2, 3> loc("test/codegen.toy":7:17)
- %1 = "toy.reshape"(%0) : (!toy.array<2, 3>) -> !toy.array<2, 3> loc("test/codegen.toy":7:3)
- %2 = "toy.constant"() {value: dense<tensor<6xf64>, [1.000000e+00, 2.000000e+00, 3.000000e+00, 4.000000e+00, 5.000000e+00, 6.000000e+00]>} : () -> !toy.array<6> loc("test/codegen.toy":8:17)
- %3 = "toy.reshape"(%2) : (!toy.array<6>) -> !toy.array<2, 3> loc("test/codegen.toy":8:3)
- %4 = "toy.generic_call"(%1, %3, %1, %3) {callee: "multiply_transpose"} : (!toy.array<2, 3>, !toy.array<2, 3>, !toy.array<2, 3>, !toy.array<2, 3>) -> !toy<"array"> loc("test/codegen.toy":9:11)
- %5 = "toy.generic_call"(%3, %1, %3, %1) {callee: "multiply_transpose"} : (!toy.array<2, 3>, !toy.array<2, 3>, !toy.array<2, 3>, !toy.array<2, 3>) -> !toy<"array"> loc("test/codegen.toy":10:11)
- "toy.print"(%5) : (!toy<"array">) -> () loc("test/codegen.toy":11:3)
- "toy.return"() : () -> () loc("test/codegen.toy":6:1)
-}
+module {
+ func @multiply_transpose(%arg0: tensor<*xf64>, %arg1: tensor<*xf64>)
+ attributes {toy.generic} {
+ %0 = "toy.transpose"(%arg1) : (tensor<*xf64>) -> tensor<*xf64> loc("test/codegen.toy":3:14)
+ %1 = "toy.mul"(%arg0, %0) : (tensor<*xf64>, tensor<*xf64>) -> tensor<*xf64> loc("test/codegen.toy":3:14)
+ "toy.return"(%1) : (tensor<*xf64>) -> () loc("test/codegen.toy":3:3)
+ } loc("test/codegen.toy":2:1)
+ func @main() {
+ %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/codegen.toy":7:17)
+ %1 = "toy.reshape"(%0) : (tensor<2x3xf64>) -> tensor<2x3xf64> loc("test/codegen.toy":7:3)
+ %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/codegen.toy":8:17)
+ %3 = "toy.reshape"(%2) : (tensor<6xf64>) -> tensor<2x3xf64> loc("test/codegen.toy":8:3)
+ %4 = "toy.generic_call"(%1, %3) {callee = @multiply_transpose} : (tensor<2x3xf64>, tensor<2x3xf64>) -> tensor<*xf64> loc("test/codegen.toy":9:11)
+ %5 = "toy.generic_call"(%3, %1) {callee = @multiply_transpose} : (tensor<2x3xf64>, tensor<2x3xf64>) -> tensor<*xf64> loc("test/codegen.toy":10:11)
+ "toy.print"(%5) : (tensor<*xf64>) -> () loc("test/codegen.toy":11:3)
+ "toy.return"() : () -> () loc("test/codegen.toy":6:1)
+ } loc("test/codegen.toy":6:1)
+} 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 > codegen.mlir` followed by `toyc-ch2 codegen.mlir
--emit=mlir`.
-
-Notice how these MLIR operations are prefixed with `toy.` ; by convention we use
-this similarly to a "namespace" in order to avoid conflicting with other
-operations with the same name. Similarly the syntax for types wraps an arbitrary
-string representing our custom types within our "namespace" `!toy<...>`. Of
-course at this point MLIR does not know anything about Toy, and so there is no
-semantic associated with the operations and types, everything is opaque and
-string-based. The only thing enforced by MLIR here is that the IR is in SSA
-form: values are defined once, and uses appears after their definition.
+-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`.
+
+At this point MLIR does not know anything about Toy, so there are no semantics
+associated with the operations, everything is opaque and string-based. The only
+thing enforced by MLIR here is that the IR is in SSA form: values are defined
+once, and uses appear after their definition.
This can be observed by crafting what should be an invalid IR for Toy and see it
round-trip without tripping the verifier:
```MLIR(.mlir)
// RUN: toyc %s -emit=mlir
+
func @main() {
- %0 = "toy.print"() : () -> !toy.array<2, 3>
+ %0 = "toy.print"() : () -> tensor<2x3xf64>
}
```
-There are multiple problems here: first the `toy.print` is not a terminator,
-then it should take an operand, and not return any value.
+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 chapter](Ch-3.md) we will register our dialect and operations with
-MLIR, plug in the verifier, and add nicer APIs to manipulate our operations.
+MLIR, plug into the verifier, and add nicer APIs to manipulate our operations.
+