2019-05-16 06:24:20 +08:00
|
|
|
# Quickstart tutorial to defining custom dialect attributes and types
|
|
|
|
|
|
|
|
This document is a quickstart to defining dialect specific extensions to the
|
|
|
|
[attribute](LangRef.md#attributes) and [type system](LangRef.md#type-system).
|
|
|
|
The main part of the tutorial focuses on defining types, but the instructions
|
|
|
|
are nearly identical for defining attributes.
|
|
|
|
|
|
|
|
See [MLIR specification](LangRef.md) for more information about MLIR, the
|
|
|
|
structure of the IR, operations, etc.
|
|
|
|
|
|
|
|
## Types
|
|
|
|
|
|
|
|
Types in MLIR (like attributes, locations, and many other things) are
|
|
|
|
value-typed. This means that instances of `Type` should be passed around
|
|
|
|
by-value, as opposed to by-pointer or by-reference. The `Type` class in itself
|
|
|
|
acts as a wrapper around an internal storage object that is uniqued within an
|
|
|
|
instance of an `MLIRContext`.
|
|
|
|
|
|
|
|
### 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 a dialect
|
|
|
|
author, this means that a range of type `kind` values must be explicitly, and
|
|
|
|
statically, reserved. A dialect can reserve a range of values by adding a new
|
|
|
|
entry to the
|
[mlir] NFC: Fix broken links in docs
Summary: This commit fixes missing links that are caused by the repository movement.
Reviewers: Jim, rriddle, jpienaar
Reviewed By: Jim, rriddle, jpienaar
Subscribers: arpith-jacob, mehdi_amini, rriddle, jpienaar, burmako, shauheen, antiagainst, nicolasvasilache, llvm-commits
Tags: #llvm
Differential Revision: https://reviews.llvm.org/D72065
2020-01-03 10:25:06 +08:00
|
|
|
[DialectSymbolRegistry](https://github.com/llvm/llvm-project/blob/master/mlir/include/mlir/IR/DialectSymbolRegistry.def).
|
2019-05-16 06:24:20 +08:00
|
|
|
To support out-of-tree and experimental dialects, the registry predefines a set
|
|
|
|
of privates ranges, `PRIVATE_EXPERIMENTAL_[0-9]`, that are free for immediate
|
|
|
|
use.
|
|
|
|
|
|
|
|
```c++
|
|
|
|
DEFINE_SYM_KIND_RANGE(LINALG) // Linear Algebra Dialect
|
|
|
|
DEFINE_SYM_KIND_RANGE(TOY) // Toy language (tutorial) Dialect
|
|
|
|
|
|
|
|
// The following ranges are reserved for experimenting with MLIR dialects in a
|
|
|
|
// private context without having to register them here.
|
|
|
|
DEFINE_SYM_KIND_RANGE(PRIVATE_EXPERIMENTAL_0)
|
|
|
|
```
|
|
|
|
|
|
|
|
For the sake of this tutorial, we will use the predefined
|
|
|
|
`PRIVATE_EXPERIMENTAL_0` range. These definitions will provide a range in the
|
|
|
|
Type::Kind enum to use when defining the derived types.
|
|
|
|
|
|
|
|
```c++
|
|
|
|
namespace MyTypes {
|
|
|
|
enum Kinds {
|
|
|
|
// These kinds will be used in the examples below.
|
|
|
|
Simple = Type::Kind::FIRST_PRIVATE_EXPERIMENTAL_0_TYPE,
|
|
|
|
Complex
|
|
|
|
};
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
### Defining the type class
|
|
|
|
|
|
|
|
As described above, `Type` objects in MLIR are value-typed and rely on having an
|
2019-09-26 02:57:13 +08:00
|
|
|
implicitly internal storage object that holds the actual data for the type. When
|
2019-05-16 06:24:20 +08:00
|
|
|
defining a new `Type` it isn't always necessary to define a new storage class.
|
|
|
|
So before defining the derived `Type`, it's important to know which of the two
|
|
|
|
classes of `Type` we are defining. Some types are `primitives` meaning they do
|
|
|
|
not have any parameters and are singletons uniqued by kind, like the
|
|
|
|
[`index` type](LangRef.md#index-type). Parametric types on the other hand, have
|
|
|
|
additional information that differentiates different instances of the same
|
|
|
|
`Type` kind. For example the [`integer` type](LangRef.md#integer-type) has a
|
|
|
|
bitwidth, making `i8` and `i16` be different instances of
|
|
|
|
[`integer` type](LangRef.md#integer-type).
|
|
|
|
|
|
|
|
#### Simple non-parametric types
|
|
|
|
|
|
|
|
For simple parameterless types, we can jump straight into defining the derived
|
|
|
|
type class. Given that these types are uniqued solely on `kind`, we don't need
|
|
|
|
to provide our own storage class.
|
|
|
|
|
|
|
|
```c++
|
|
|
|
/// This class defines a simple parameterless type. All derived types must
|
|
|
|
/// inherit from the CRTP class 'Type::TypeBase'. It takes as template
|
|
|
|
/// parameters the concrete type (SimpleType), and the base class to use (Type).
|
|
|
|
/// 'Type::TypeBase' also provides several utility methods to simplify type
|
|
|
|
/// construction.
|
|
|
|
class SimpleType : public Type::TypeBase<SimpleType, Type> {
|
|
|
|
public:
|
|
|
|
/// Inherit some necessary constructors from 'TypeBase'.
|
|
|
|
using Base::Base;
|
|
|
|
|
|
|
|
/// This static method is used to support type inquiry through isa, cast,
|
|
|
|
/// and dyn_cast.
|
|
|
|
static bool kindof(unsigned kind) { return kind == MyTypes::Simple; }
|
|
|
|
|
|
|
|
/// This method is used to get an instance of the 'SimpleType'. Given that
|
|
|
|
/// this is a parameterless type, it just needs to take the context for
|
|
|
|
/// uniquing purposes.
|
|
|
|
static SimpleType get(MLIRContext *context) {
|
|
|
|
// Call into a helper 'get' method in 'TypeBase' to get a uniqued instance
|
|
|
|
// of this type.
|
|
|
|
return Base::get(context, MyTypes::Simple);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
```
|
|
|
|
|
|
|
|
#### Parametric types
|
|
|
|
|
|
|
|
Parametric types are those that have additional construction or uniquing
|
|
|
|
constraints outside of the type `kind`. As such, these types require defining a
|
|
|
|
type storage class.
|
|
|
|
|
|
|
|
##### Defining a type storage
|
|
|
|
|
|
|
|
Type storage objects contain all of the data necessary to construct and unique a
|
|
|
|
parametric type instance. The storage classes must obey the following:
|
|
|
|
|
|
|
|
* Inherit from the base type storage class `TypeStorage`.
|
|
|
|
* Define a type alias, `KeyTy`, that maps to a type that uniquely identifies
|
|
|
|
an instance of the parent type.
|
|
|
|
* Provide a construction method that is used to allocate a new instance of the
|
|
|
|
storage class.
|
|
|
|
- `Storage *construct(TypeStorageAllocator &, const KeyTy &key)`
|
|
|
|
* Provide a comparison method between the storage and `KeyTy`.
|
|
|
|
- `bool operator==(const KeyTy &) const`
|
|
|
|
* Provide a method to generate the `KeyTy` from a list of arguments passed to
|
|
|
|
the uniquer. (Note: This is only necessary if the `KeyTy` cannot be default
|
|
|
|
constructed from these arguments).
|
|
|
|
- `static KeyTy getKey(Args...&& args)`
|
|
|
|
* Provide a method to hash an instance of the `KeyTy`. (Note: This is not
|
|
|
|
necessary if an `llvm::DenseMapInfo<KeyTy>` specialization exists)
|
|
|
|
- `static llvm::hash_code hashKey(const KeyTy &)`
|
|
|
|
|
|
|
|
Let's look at an example:
|
|
|
|
|
|
|
|
```c++
|
|
|
|
/// Here we define a storage class for a ComplexType, that holds a non-zero
|
|
|
|
/// integer and an integer type.
|
|
|
|
struct ComplexTypeStorage : public TypeStorage {
|
|
|
|
ComplexTypeStorage(unsigned nonZeroParam, Type integerType)
|
|
|
|
: nonZeroParam(nonZeroParam), integerType(integerType) {}
|
|
|
|
|
|
|
|
/// The hash key for this storage is a pair of the integer and type params.
|
|
|
|
using KeyTy = std::pair<unsigned, Type>;
|
|
|
|
|
|
|
|
/// Define the comparison function for the key type.
|
|
|
|
bool operator==(const KeyTy &key) const {
|
|
|
|
return key == KeyTy(nonZeroParam, integerType);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Define a hash function for the key type.
|
|
|
|
/// Note: This isn't necessary because std::pair, unsigned, and Type all have
|
|
|
|
/// hash functions already available.
|
|
|
|
static llvm::hash_code hashKey(const KeyTy &key) {
|
|
|
|
return llvm::hash_combine(key.first, key.second);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Define a construction function for the key type.
|
|
|
|
/// Note: This isn't necessary because KeyTy can be directly constructed with
|
|
|
|
/// the given parameters.
|
|
|
|
static KeyTy getKey(unsigned nonZeroParam, Type integerType) {
|
|
|
|
return KeyTy(nonZeroParam, integerType);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Define a construction method for creating a new instance of this storage.
|
|
|
|
static ComplexTypeStorage *construct(TypeStorageAllocator &allocator,
|
|
|
|
const KeyTy &key) {
|
|
|
|
return new (allocator.allocate<ComplexTypeStorage>())
|
|
|
|
ComplexTypeStorage(key.first, key.second);
|
|
|
|
}
|
|
|
|
|
|
|
|
unsigned nonZeroParam;
|
|
|
|
Type integerType;
|
|
|
|
};
|
|
|
|
```
|
|
|
|
|
|
|
|
##### Type class definition
|
|
|
|
|
|
|
|
Now that the storage class has been created, the derived type class can be
|
|
|
|
defined. This structure is similar to the
|
|
|
|
[simple type](#simple-non-parametric-types), except for a bit more of the
|
|
|
|
functionality of `Type::TypeBase` is put to use.
|
|
|
|
|
|
|
|
```c++
|
|
|
|
/// This class defines a parametric type. All derived types must inherit from
|
|
|
|
/// the CRTP class 'Type::TypeBase'. It takes as template parameters the
|
|
|
|
/// concrete type (ComplexType), the base class to use (Type), and the storage
|
|
|
|
/// class (ComplexTypeStorage). 'Type::TypeBase' also provides several utility
|
|
|
|
/// methods to simplify type construction and verification.
|
|
|
|
class ComplexType : public Type::TypeBase<ComplexType, Type,
|
|
|
|
ComplexTypeStorage> {
|
|
|
|
public:
|
|
|
|
/// Inherit some necessary constructors from 'TypeBase'.
|
|
|
|
using Base::Base;
|
|
|
|
|
|
|
|
/// This static method is used to support type inquiry through isa, cast,
|
|
|
|
/// and dyn_cast.
|
|
|
|
static bool kindof(unsigned kind) { return kind == MyTypes::Complex; }
|
|
|
|
|
|
|
|
/// This method is used to get an instance of the 'ComplexType'. This method
|
|
|
|
/// asserts that all of the construction invariants were satisfied. To
|
|
|
|
/// gracefully handle failed construction, getChecked should be used instead.
|
|
|
|
static ComplexType get(MLIRContext *context, unsigned param, Type type) {
|
|
|
|
// Call into a helper 'get' method in 'TypeBase' to get a uniqued instance
|
|
|
|
// of this type. All parameters to the storage class are passed after the
|
|
|
|
// type kind.
|
|
|
|
return Base::get(context, MyTypes::Complex, param, type);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// This method is used to get an instance of the 'ComplexType', defined at
|
|
|
|
/// the given location. If any of the construction invariants are invalid,
|
|
|
|
/// errors are emitted with the provided location and a null type is returned.
|
|
|
|
/// Note: This method is completely optional.
|
|
|
|
static ComplexType getChecked(MLIRContext *context, unsigned param, Type type,
|
|
|
|
Location location) {
|
|
|
|
// Call into a helper 'getChecked' method in 'TypeBase' to get a uniqued
|
|
|
|
// instance of this type. All parameters to the storage class are passed
|
|
|
|
// after the type kind.
|
|
|
|
return Base::getChecked(location, context, MyTypes::Complex, param, type);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// This method is used to verify the construction invariants passed into the
|
|
|
|
/// 'get' and 'getChecked' methods. Note: This method is completely optional.
|
|
|
|
static LogicalResult verifyConstructionInvariants(
|
|
|
|
llvm::Optional<Location> loc, MLIRContext *context, unsigned param,
|
|
|
|
Type type) {
|
|
|
|
// Our type only allows non-zero parameters.
|
|
|
|
if (param == 0) {
|
|
|
|
if (loc)
|
|
|
|
context->emitError(loc) << "non-zero parameter passed to 'ComplexType'";
|
|
|
|
return failure();
|
|
|
|
}
|
|
|
|
// Our type also expects an integer type.
|
|
|
|
if (!type.isa<IntegerType>()) {
|
|
|
|
if (loc)
|
|
|
|
context->emitError(loc) << "non integer-type passed to 'ComplexType'";
|
|
|
|
return failure();
|
|
|
|
}
|
|
|
|
return success();
|
|
|
|
}
|
2019-05-30 04:23:47 +08:00
|
|
|
|
|
|
|
/// Return the parameter value.
|
|
|
|
unsigned getParameter() {
|
|
|
|
// 'getImpl' returns a pointer to our internal storage instance.
|
|
|
|
return getImpl()->nonZeroParam;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Return the integer parameter type.
|
|
|
|
IntegerType getParameterType() {
|
|
|
|
// 'getImpl' returns a pointer to our internal storage instance.
|
|
|
|
return getImpl()->integerType;
|
|
|
|
}
|
2019-05-16 06:24:20 +08:00
|
|
|
};
|
|
|
|
```
|
|
|
|
|
|
|
|
### Registering types with a Dialect
|
|
|
|
|
|
|
|
Once the dialect types have been defined, they must then be registered with a
|
|
|
|
`Dialect`. This is done via similar mechanism to
|
|
|
|
[operations](LangRef.md#operations), `addTypes`.
|
|
|
|
|
|
|
|
```c++
|
|
|
|
struct MyDialect : public Dialect {
|
|
|
|
MyDialect(MLIRContext *context) : Dialect(/*name=*/"mydialect", context) {
|
2019-09-26 02:57:13 +08:00
|
|
|
/// Add these types to the dialect.
|
2019-05-16 06:24:20 +08:00
|
|
|
addTypes<SimpleType, ComplexType>();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
```
|
|
|
|
|
|
|
|
### Parsing and Printing
|
|
|
|
|
|
|
|
As a final step after registration, a dialect must override the `printType` and
|
|
|
|
`parseType` hooks. These enable native support for roundtripping the type in the
|
|
|
|
textual IR.
|
|
|
|
|
|
|
|
## Attributes
|
|
|
|
|
|
|
|
As stated in the introduction, the process for defining dialect attributes is
|
|
|
|
nearly identical to that of defining dialect types. That key difference is that
|
|
|
|
the things named `*Type` are generally now named `*Attr`.
|
|
|
|
|
|
|
|
* `Type::TypeBase` -> `Attribute::AttrBase`
|
|
|
|
* `TypeStorageAllocator` -> `AttributeStorageAllocator`
|
|
|
|
* `addTypes` -> `addAttributes`
|
|
|
|
|
|
|
|
Aside from that, all of the interfaces for uniquing and storage construction are
|
|
|
|
all the same.
|