| # Defining Dialects |
| |
| This document describes how to define [Dialects](../LangRef.md/#dialects). |
| |
| [TOC] |
| |
| ## LangRef Refresher |
| |
| Before diving into how to define these constructs, below is a quick refresher |
| from the [MLIR LangRef](../LangRef.md). |
| |
| Dialects are the mechanism by which to engage with and extend the MLIR |
| ecosystem. They allow for defining new [attributes](../LangRef.md/#attributes), |
| [operations](../LangRef.md/#operations), and [types](../LangRef.md/#type-system). |
| Dialects are used to model a variety of different abstractions; from traditional |
| [arithmetic](../Dialects/ArithOps.md) to |
| [pattern rewrites](../Dialects/PDLOps.md); and is one of the most fundamental |
| aspects of MLIR. |
| |
| ## Defining a Dialect |
| |
| At the most fundamental level, defining a dialect in MLIR is as simple as |
| specializing the |
| [C++ `Dialect` class](https://github.com/llvm/llvm-project/blob/main/mlir/include/mlir/IR/Dialect.h). |
| That being said, MLIR provides a powerful declaratively specification mechanism via |
| [TableGen](https://llvm.org/docs/TableGen/index.html); a generic language with |
| tooling to maintain records of domain-specific information; that simplifies the |
| definition process by automatically generating all of the necessary boilerplate |
| C++ code, significantly reduces maintainence burden when changing aspects of dialect |
| definitions, and also provides additional tools on top (such as |
| documentation generation). Given the above, the declarative specification is the |
| expected mechanism for defining new dialects, and is the method detailed within |
| this document. Before continuing, it is highly recommended that users review the |
| [TableGen Programmer's Reference](https://llvm.org/docs/TableGen/ProgRef.html) |
| for an introduction to its syntax and constructs. |
| |
| Below showcases an example simple Dialect definition. We generally recommend defining |
| the Dialect class in a different `.td` file from the attributes, operations, types, |
| and other sub-components of the dialect to establish a proper layering between |
| the various different dialect components. It also prevents situations where you may |
| inadvertantly generate multiple definitions for some constructs. This recommendation |
| extends to all of the MLIR constructs, including [Interfaces](../Interfaces.md) for example. |
| |
| ```tablegen |
| // Include the definition of the necessary tablegen constructs for defining |
| // our dialect. |
| include "mlir/IR/DialectBase.td" |
| |
| // Here is a simple definition of a dialect. |
| def MyDialect : Dialect { |
| let summary = "A short one line description of my dialect."; |
| let description = [{ |
| My dialect is a very important dialect. This section contains a much more |
| detailed description that documents all of the important pieces of information |
| to know about the document. |
| }]; |
| |
| /// This is the namespace of the dialect. It is used to encapsulate the sub-components |
| /// of the dialect, such as operations ("my_dialect.foo"). |
| let name = "my_dialect"; |
| |
| /// The C++ namespace that the dialect, and its sub-components, get placed in. |
| let cppNamespace = "::my_dialect"; |
| } |
| ``` |
| |
| The above showcases a very simple description of a dialect, but dialects have lots |
| of other capabilities that you may or may not need to utilize. |
| |
| ### Initialization |
| |
| Every dialect must implement an initialization hook to add attributes, operations, types, |
| attach any desired interfaces, or perform any other necessary initialization for the |
| dialect that should happen on construction. This hook is declared for every dialect to |
| define, and has the form: |
| |
| ```c++ |
| void MyDialect::initialize() { |
| // Dialect initialization logic should be defined in here. |
| } |
| ``` |
| |
| ### Documentation |
| |
| The `summary` and `description` fields allow for providing user documentation |
| for the dialect. The `summary` field expects a simple single-line string, with the |
| `description` field used for long and extensive documentation. This documentation can be |
| used to generate markdown documentation for the dialect and is used by upstream |
| [MLIR dialects](https://mlir.llvm.org/docs/Dialects/). |
| |
| ### Class Name |
| |
| The name of the C++ class which gets generated is the same as the name of our TableGen |
| dialect definition, but with any `_` characters stripped out. This means that if you name |
| your dialect `Foo_Dialect`, the generated C++ class would be `FooDialect`. In the example |
| above, we would get a C++ dialect named `MyDialect`. |
| |
| ### C++ Namespace |
| |
| The namespace that the C++ class for our dialect, and all of its sub-components, is placed |
| under is specified by the `cppNamespace` field. By default, uses the name of the dialect as |
| the only namespace. To avoid placing in any namespace, use `""`. To specify nested namespaces, |
| use `"::"` as the delimiter between namespace, e.g., given `"A::B"`, C++ classes will be placed |
| within: `namespace A { namespace B { <classes> } }`. |
| |
| Note that this works in conjunction with the dialect's C++ code. Depending on how the generated files |
| are included, you may want to specify a full namespace path or a partial one. In general, it's best |
| to use full namespaces whenever you can. This makes it easier for dialects within different namespaces, |
| and projects, to interact with each other. |
| |
| ### C++ Accessor Generation |
| |
| When generating accessors for dialects and their components (attributes, operations, types, etc.), |
| we prefix the name with `get` and `set` respectively, and transform `snake_style` names to camel |
| case (`UpperCamel` when prefixed, and `lowerCamel` for individual variable names). For example, if an |
| operation were defined as: |
| |
| ```tablegen |
| def MyOp : MyDialect<"op"> { |
| let arguments = (ins StrAttr:$value, StrAttr:$other_value); |
| } |
| ``` |
| |
| It would have accessors generated for the `value` and `other_value` attributes as follows: |
| |
| ```c++ |
| StringAttr MyOp::getValue(); |
| void MyOp::setValue(StringAttr newValue); |
| |
| StringAttr MyOp::getOtherValue(); |
| void MyOp::setOtherValue(StringAttr newValue); |
| ``` |
| |
| ### Dependent Dialects |
| |
| MLIR has a very large ecosystem, and contains dialects that serve many different purposes. It |
| is quite common, given the above, that dialects may want to reuse certain components from other |
| dialects. This may mean generating operations from those dialects during canonicalization, reusing |
| attributes or types, etc. When a dialect has a dependency on another, i.e. when it constructs and/or |
| generally relies on the components of another dialect, a dialect dependency should be explicitly |
| recorded. An explicitly dependency ensures that dependent dialects are loaded alongside the |
| dialect. Dialect dependencies can be recorded using the `dependentDialects` dialects field: |
| |
| ```tablegen |
| def MyDialect : Dialect { |
| // Here we register the Arithmetic and Func dialect as dependencies of our `MyDialect`. |
| let dependentDialects = [ |
| "arith::ArithDialect", |
| "func::FuncDialect" |
| ]; |
| } |
| ``` |
| |
| ### Extra declarations |
| |
| The declarative Dialect definitions try to auto-generate as much logic and methods |
| as possible. With that said, there will always be long-tail cases that won't be covered. |
| For such cases, `extraClassDeclaration` can be used. Code within the `extraClassDeclaration` |
| field will be copied literally to the generated C++ Dialect class. |
| |
| Note that `extraClassDeclaration` is a mechanism intended for long-tail cases by |
| power users; for not-yet-implemented widely-applicable cases, improving the |
| infrastructure is preferable. |
| |
| ### `hasConstantMaterializer`: Materializing Constants from Attributes |
| |
| This field is utilized to materialize a constant operation from an `Attribute` value and |
| a `Type`. This is generally used when an operation within this dialect has been folded, |
| and a constant operation should be generated. `hasConstantMaterializer` is used to enable |
| materialization, and the `materializeConstant` hook is declared on the dialect. This |
| hook takes in an `Attribute` value, generally returned by `fold`, and produces a |
| "constant-like" operation that materializes that value. See the |
| [documentation for canonicalization](../Canonicalization.md) for a more in-depth |
| introduction to `folding` in MLIR. |
| |
| Constant materialization logic can then be defined in the source file: |
| |
| ```c++ |
| /// Hook to materialize a single constant operation from a given attribute value |
| /// with the desired resultant type. This method should use the provided builder |
| /// to create the operation without changing the insertion position. The |
| /// generated operation is expected to be constant-like. On success, this hook |
| /// should return the operation generated to represent the constant value. |
| /// Otherwise, it should return nullptr on failure. |
| Operation *MyDialect::materializeConstant(OpBuilder &builder, Attribute value, |
| Type type, Location loc) { |
| ... |
| } |
| ``` |
| |
| ### `hasNonDefaultDestructor`: Providing a custom destructor |
| |
| This field should be used when the Dialect class has a custom destructor, i.e. |
| when the dialect has some special logic to be run in the `~MyDialect`. In this case, |
| only the declaration of the destructor is generated for the Dialect class. |
| |
| ### Discardable Attribute Verification |
| |
| As described by the [MLIR Language Reference](../LangRef.md/#attributes), |
| *discardable attribute* are a type of attribute that has its semantics defined |
| by the dialect whose name prefixes that of the attribute. For example, if an |
| operation has an attribute named `gpu.contained_module`, the `gpu` dialect |
| defines the semantics and invariants, such as when and where it is valid to use, |
| of that attribute. To hook into this verification for attributes that are prefixed |
| by our dialect, several hooks on the Dialect may be used: |
| |
| #### `hasOperationAttrVerify` |
| |
| This field generates the hook for verifying when a discardable attribute of this dialect |
| has been used within the attribute dictionary of an operation. This hook has the form: |
| |
| ```c++ |
| /// Verify the use of the given attribute, whose name is prefixed by the namespace of this |
| /// dialect, that was used in `op`s dictionary. |
| LogicalResult MyDialect::verifyOperationAttribute(Operation *op, NamedAttribute attribute); |
| ``` |
| |
| #### `hasRegionArgAttrVerify` |
| |
| This field generates the hook for verifying when a discardable attribute of this dialect |
| has been used within the attribute dictionary of a region entry block argument. Note that |
| the block arguments of a region entry block do not themselves have attribute dictionaries, |
| but some operations may provide special dictionary attributes that correspond to the arguments |
| of a region. For example, operations that implement `FunctionOpInterface` may have attribute |
| dictionaries on the operation that correspond to the arguments of entry block of the function. |
| In these cases, those operations will invoke this hook on the dialect to ensure the attribute |
| is verified. The hook necessary for the dialect to implement has the form: |
| |
| ```c++ |
| /// Verify the use of the given attribute, whose name is prefixed by the namespace of this |
| /// dialect, that was used on the attribute dictionary of a region entry block argument. |
| /// Note: As described above, when a region entry block has a dictionary is up to the individual |
| /// operation to define. |
| LogicalResult MyDialect::verifyRegionArgAttribute(Operation *op, unsigned regionIndex, |
| unsigned argIndex, NamedAttribute attribute); |
| ``` |
| |
| #### `hasRegionResultAttrVerify` |
| |
| This field generates the hook for verifying when a discardable attribute of this dialect |
| has been used within the attribute dictionary of a region result. Note that the results of a |
| region do not themselves have attribute dictionaries, but some operations may provide special |
| dictionary attributes that correspond to the results of a region. For example, operations that |
| implement `FunctionOpInterface` may have attribute dictionaries on the operation that correspond |
| to the results of the function. In these cases, those operations will invoke this hook on the |
| dialect to ensure the attribute is verified. The hook necessary for the dialect to implement |
| has the form: |
| |
| ```c++ |
| /// Generate verification for the given attribute, whose name is prefixed by the namespace |
| /// of this dialect, that was used on the attribute dictionary of a region result. |
| /// Note: As described above, when a region entry block has a dictionary is up to the individual |
| /// operation to define. |
| LogicalResult MyDialect::verifyRegionResultAttribute(Operation *op, unsigned regionIndex, |
| unsigned argIndex, NamedAttribute attribute); |
| ``` |
| |
| ### Operation Interface Fallback |
| |
| Some dialects have an open ecosystem and don't register all of the possible operations. In such |
| cases it is still possible to provide support for implementing an `OpInterface` for these |
| operations. When an operation isn't registered or does not provide an implementation for an |
| interface, the query will fallback to the dialect itself. The `hasOperationInterfaceFallback` |
| field may be used to declare this fallback for operations: |
| |
| ```c++ |
| /// Return an interface model for the interface with the given `typeId` for the operation |
| /// with the given name. |
| void *MyDialect::getRegisteredInterfaceForOp(TypeID typeID, StringAttr opName); |
| ``` |
| |
| For a more detail description of the expected usages of this hook, view the detailed |
| [interface documentation](../Interfaces.md/#dialect-fallback-for-opinterface). |
| |
| ### Default Attribute/Type Parsers and Printers |
| |
| When a dialect registers an Attribute or Type, it must also override the respective |
| `Dialect::parseAttribute`/`Dialect::printAttribute` or |
| `Dialect::parseType`/`Dialect::printType` methods. In these cases, the dialect must |
| explicitly handle the parsing and printing of each individual attribute or type within |
| the dialect. If all of the attributes and types of the dialect provide a mnemonic, |
| however, these methods may be autogenerated by using the |
| `useDefaultAttributePrinterParser` and `useDefaultTypePrinterParser` fields. By default, |
| these fields are set to `1`(enabled), meaning that if a dialect needs to explicitly handle the |
| parser and printer of its Attributes and Types it should set these to `0` as necessary. |
| |
| ### Dialect-wide Canonicalization Patterns |
| |
| Generally, [canonicalization](../Canonicalization.md) patterns are specific to individual |
| operations within a dialect. There are some cases, however, that prompt canonicalization |
| patterns to be added to the dialect-level. For example, if a dialect defines a canonicalization |
| pattern that operates on an interface or trait, it can be beneficial to only add this pattern |
| once, instead of duplicating per-operation that implements that interface. To enable the |
| generation of this hook, the `hasCanonicalizer` field may be used. This will declare |
| the `getCanonicalizationPatterns` method on the dialect, which has the form: |
| |
| ```c++ |
| /// Return the canonicalization patterns for this dialect: |
| void MyDialect::getCanonicalizationPatterns(RewritePatternSet &results) const; |
| ``` |
| |
| See the documentation for [Canonicalization in MLIR](../Canonicalization.md) for |
| a more detailed description about canonicalization patterns. |
| |
| ### Defining bytecode format for dialect attributes and types |
| |
| By default bytecode serialization of dialect attributes and types uses the |
| regular textual format. Dialects can define a more compact bytecode format for |
| the attributes and types in dialect by defining & attaching |
| `BytecodeDialectInterface` to the dialect. Basic support for generating |
| readers/writers for the bytecode dialect interface can be generated using ODS's |
| `-gen-bytecode`. The rest of the section will show an example. |
| |
| One can define the printing and parsing for a type in dialect `Foo` as follow: |
| |
| ```td |
| include "mlir/IR/BytecodeBase.td" |
| |
| let cType = "MemRefType" in { |
| // Written in pseudo code showing the lowered encoding: |
| // /// MemRefType { |
| // /// shape: svarint[], |
| // /// elementType: Type, |
| // /// layout: Attribute |
| // /// } |
| // /// |
| // and the enum value: |
| // kMemRefType = 1, |
| // |
| // The corresponding definition in the ODS generator: |
| def MemRefType : DialectType<(type |
| Array<SignedVarInt>:$shape, |
| Type:$elementType, |
| MemRefLayout:$layout |
| )> { |
| let printerPredicate = "!$_val.getMemorySpace()"; |
| } |
| |
| // /// MemRefTypeWithMemSpace { |
| // /// memorySpace: Attribute, |
| // /// shape: svarint[], |
| // /// elementType: Type, |
| // /// layout: Attribute |
| // /// } |
| // /// Variant of MemRefType with non-default memory space. |
| // kMemRefTypeWithMemSpace = 2, |
| def MemRefTypeWithMemSpace : DialectType<(type |
| Attribute:$memorySpace, |
| Array<SignedVarInt>:$shape, |
| Type:$elementType, |
| MemRefLayout:$layout |
| )> { |
| let printerPredicate = "!!$_val.getMemorySpace()"; |
| // Note: order of serialization does not match order of builder. |
| let cBuilder = "get<$_resultType>(context, shape, elementType, layout, memorySpace)"; |
| } |
| } |
| |
| def FooDialectTypes : DialectTypes<"Foo"> { |
| let elems = [ |
| ReservedOrDead, // assigned index 0 |
| MemRefType, // assigned index 1 |
| MemRefTypeWithMemSpace, // assigned index 2 |
| ... |
| ]; |
| } |
| ... |
| ``` |
| |
| Here we have: |
| |
| * An outer most `cType` as we are representing encoding one C++ type using two |
| different variants. |
| * The different `DialectType` instances are differentiated in printing by the |
| printer predicate while parsing the different variant is already encoded and |
| different builder functions invoked. |
| * Custom `cBuilder` is specified as the way its laid out on disk in the |
| bytecode doesn't match the order of arguments to the build methods of the |
| type. |
| * Many of the common dialect bytecode reading and writing atoms (such as |
| `VarInt`, `SVarInt`, `Blob`) are defined in `BytecodeBase` while one can |
| also define custom forms or combine via `CompositeBytecode` instances. |
| * `ReservedOrDead` is a special keyword to indicate a skipped enum instance |
| for which no read/write or dispatch code is generated. |
| * `Array` is a helper method for which during printing a list is serialized |
| (e.g., a varint of number of items followed by said number of items) or |
| parsed. |
| |
| The generated code consists of a four standalone methods with which the |
| following interface can define the bytecode dialect interface: |
| |
| ```c++ |
| #include "mlir/Dialect/Foo/FooDialectBytecode.cpp.inc" |
| |
| struct FooDialectBytecodeInterface : public BytecodeDialectInterface { |
| FooDialectBytecodeInterface(Dialect *dialect) |
| : BytecodeDialectInterface(dialect) {} |
| |
| //===--------------------------------------------------------------------===// |
| // Attributes |
| |
| Attribute readAttribute(DialectBytecodeReader &reader) const override { |
| return ::readAttribute(getContext(), reader); |
| } |
| |
| LogicalResult writeAttribute(Attribute attr, |
| DialectBytecodeWriter &writer) const override { |
| return ::writeAttribute(attr, writer); |
| } |
| |
| //===--------------------------------------------------------------------===// |
| // Types |
| |
| Type readType(DialectBytecodeReader &reader) const override { |
| return ::readType(getContext(), reader); |
| } |
| |
| LogicalResult writeType(Type type, |
| DialectBytecodeWriter &writer) const override { |
| return ::writeType(type, writer); |
| } |
| }; |
| ``` |
| |
| along with defining the corresponding build rules to invoke generator |
| (`-gen-bytecode -bytecode-dialect="Quant"`). |
| |
| ## Defining an Extensible dialect |
| |
| This section documents the design and API of the extensible dialects. Extensible |
| dialects are dialects that can be extended with new operations and types defined |
| at runtime. This allows for users to define dialects via with meta-programming, |
| or from another language, without having to recompile C++ code. |
| |
| ### Defining an extensible dialect |
| |
| Dialects defined in C++ can be extended with new operations, types, etc., at |
| runtime by inheriting from `mlir::ExtensibleDialect` instead of `mlir::Dialect` |
| (note that `ExtensibleDialect` inherits from `Dialect`). The `ExtensibleDialect` |
| class contains the necessary fields and methods to extend the dialect at |
| runtime. |
| |
| ```c++ |
| class MyDialect : public mlir::ExtensibleDialect { |
| ... |
| } |
| ``` |
| |
| For dialects defined in TableGen, this is done by setting the `isExtensible` |
| flag to `1`. |
| |
| ```tablegen |
| def Test_Dialect : Dialect { |
| let isExtensible = 1; |
| ... |
| } |
| ``` |
| |
| An extensible `Dialect` can be casted back to `ExtensibleDialect` using |
| `llvm::dyn_cast`, or `llvm::cast`: |
| |
| ```c++ |
| if (auto extensibleDialect = llvm::dyn_cast<ExtensibleDialect>(dialect)) { |
| ... |
| } |
| ``` |
| |
| ### Defining a dynamic dialect |
| |
| Dynamic dialects are extensible dialects that can be defined at runtime. They |
| are only populated with dynamic operations, types, and attributes. They can be |
| registered in a `DialectRegistry` with `insertDynamic`. |
| |
| ```c++ |
| auto populateDialect = [](MLIRContext *ctx, DynamicDialect* dialect) { |
| // Code that will be ran when the dynamic dialect is created and loaded. |
| // For instance, this is where we register the dynamic operations, types, and |
| // attributes of the dialect. |
| ... |
| } |
| |
| registry.insertDynamic("dialectName", populateDialect); |
| ``` |
| |
| Once a dynamic dialect is registered in the `MLIRContext`, it can be retrieved |
| with `getOrLoadDialect`. |
| |
| ```c++ |
| Dialect *dialect = ctx->getOrLoadDialect("dialectName"); |
| ``` |
| |
| ### Defining an operation at runtime |
| |
| The `DynamicOpDefinition` class represents the definition of an operation |
| defined at runtime. It is created using the `DynamicOpDefinition::get` |
| functions. An operation defined at runtime must provide a name, a dialect in |
| which the operation will be registered in, an operation verifier. It may also |
| optionally define a custom parser and a printer, fold hook, and more. |
| |
| ```c++ |
| // The operation name, without the dialect name prefix. |
| StringRef name = "my_operation_name"; |
| |
| // The dialect defining the operation. |
| Dialect* dialect = ctx->getOrLoadDialect<MyDialect>(); |
| |
| // Operation verifier definition. |
| AbstractOperation::VerifyInvariantsFn verifyFn = [](Operation* op) { |
| // Logic for the operation verification. |
| ... |
| } |
| |
| // Parser function definition. |
| AbstractOperation::ParseAssemblyFn parseFn = |
| [](OpAsmParser &parser, OperationState &state) { |
| // Parse the operation, given that the name is already parsed. |
| ... |
| }; |
| |
| // Printer function |
| auto printFn = [](Operation *op, OpAsmPrinter &printer) { |
| printer << op->getName(); |
| // Print the operation, given that the name is already printed. |
| ... |
| }; |
| |
| // General folder implementation, see AbstractOperation::foldHook for more |
| // information. |
| auto foldHookFn = [](Operation * op, ArrayRef<Attribute> operands, |
| SmallVectorImpl<OpFoldResult> &result) { |
| ... |
| }; |
| |
| // Returns any canonicalization pattern rewrites that the operation |
| // supports, for use by the canonicalization pass. |
| auto getCanonicalizationPatterns = |
| [](RewritePatternSet &results, MLIRContext *context) { |
| ... |
| } |
| |
| // Definition of the operation. |
| std::unique_ptr<DynamicOpDefinition> opDef = |
| DynamicOpDefinition::get(name, dialect, std::move(verifyFn), |
| std::move(parseFn), std::move(printFn), std::move(foldHookFn), |
| std::move(getCanonicalizationPatterns)); |
| ``` |
| |
| Once the operation is defined, it can be registered by an `ExtensibleDialect`: |
| |
| ```c++ |
| extensibleDialect->registerDynamicOperation(std::move(opDef)); |
| ``` |
| |
| Note that the `Dialect` given to the operation should be the one registering |
| the operation. |
| |
| ### Using an operation defined at runtime |
| |
| It is possible to match on an operation defined at runtime using their names: |
| |
| ```c++ |
| if (op->getName().getStringRef() == "my_dialect.my_dynamic_op") { |
| ... |
| } |
| ``` |
| |
| An operation defined at runtime can be created by instantiating an |
| `OperationState` with the operation name, and using it with a rewriter |
| (for instance a `PatternRewriter`) to create the operation. |
| |
| ```c++ |
| OperationState state(location, "my_dialect.my_dynamic_op", |
| operands, resultTypes, attributes); |
| |
| rewriter.createOperation(state); |
| ``` |
| |
| ### Defining a type at runtime |
| |
| Contrary to types defined in C++ or in TableGen, types defined at runtime can |
| only have as argument a list of `Attribute`. |
| |
| Similarily to operations, a type is defined at runtime using the class |
| `DynamicTypeDefinition`, which is created using the `DynamicTypeDefinition::get` |
| functions. A type definition requires a name, the dialect that will register the |
| type, and a parameter verifier. It can also define optionally a custom parser |
| and printer for the arguments (the type name is assumed to be already |
| parsed/printed). |
| |
| ```c++ |
| // The type name, without the dialect name prefix. |
| StringRef name = "my_type_name"; |
| |
| // The dialect defining the type. |
| Dialect* dialect = ctx->getOrLoadDialect<MyDialect>(); |
| |
| // The type verifier. |
| // A type defined at runtime has a list of attributes as parameters. |
| auto verifier = [](function_ref<InFlightDiagnostic()> emitError, |
| ArrayRef<Attribute> args) { |
| ... |
| }; |
| |
| // The type parameters parser. |
| auto parser = [](DialectAsmParser &parser, |
| llvm::SmallVectorImpl<Attribute> &parsedParams) { |
| ... |
| }; |
| |
| // The type parameters printer. |
| auto printer =[](DialectAsmPrinter &printer, ArrayRef<Attribute> params) { |
| ... |
| }; |
| |
| std::unique_ptr<DynamicTypeDefinition> typeDef = |
| DynamicTypeDefinition::get(std::move(name), std::move(dialect), |
| std::move(verifier), std::move(printer), |
| std::move(parser)); |
| ``` |
| |
| If the printer and the parser are ommited, a default parser and printer is |
| generated with the format `!dialect.typename<arg1, arg2, ..., argN>`. |
| |
| The type can then be registered by the `ExtensibleDialect`: |
| |
| ```c++ |
| dialect->registerDynamicType(std::move(typeDef)); |
| ``` |
| |
| ### Parsing types defined at runtime in an extensible dialect |
| |
| `parseType` methods generated by TableGen can parse types defined at runtime, |
| though overriden `parseType` methods need to add the necessary support for them. |
| |
| ```c++ |
| Type MyDialect::parseType(DialectAsmParser &parser) const { |
| ... |
| |
| // The type name. |
| StringRef typeTag; |
| if (failed(parser.parseKeyword(&typeTag))) |
| return Type(); |
| |
| // Try to parse a dynamic type with 'typeTag' name. |
| Type dynType; |
| auto parseResult = parseOptionalDynamicType(typeTag, parser, dynType); |
| if (parseResult.has_value()) { |
| if (succeeded(parseResult.getValue())) |
| return dynType; |
| return Type(); |
| } |
| |
| ... |
| } |
| ``` |
| |
| ### Using a type defined at runtime |
| |
| Dynamic types are instances of `DynamicType`. It is possible to get a dynamic |
| type with `DynamicType::get` and `ExtensibleDialect::lookupTypeDefinition`. |
| |
| ```c++ |
| auto typeDef = extensibleDialect->lookupTypeDefinition("my_dynamic_type"); |
| ArrayRef<Attribute> params = ...; |
| auto type = DynamicType::get(typeDef, params); |
| ``` |
| |
| It is also possible to cast a `Type` known to be defined at runtime to a |
| `DynamicType`. |
| |
| ```c++ |
| auto dynType = cast<DynamicType>(type); |
| auto typeDef = dynType.getTypeDef(); |
| auto args = dynType.getParams(); |
| ``` |
| |
| ### Defining an attribute at runtime |
| |
| Similar to types defined at runtime, attributes defined at runtime can only have |
| as argument a list of `Attribute`. |
| |
| Similarly to types, an attribute is defined at runtime using the class |
| `DynamicAttrDefinition`, which is created using the `DynamicAttrDefinition::get` |
| functions. An attribute definition requires a name, the dialect that will |
| register the attribute, and a parameter verifier. It can also define optionally |
| a custom parser and printer for the arguments (the attribute name is assumed to |
| be already parsed/printed). |
| |
| ```c++ |
| // The attribute name, without the dialect name prefix. |
| StringRef name = "my_attribute_name"; |
| |
| // The dialect defining the attribute. |
| Dialect* dialect = ctx->getOrLoadDialect<MyDialect>(); |
| |
| // The attribute verifier. |
| // An attribute defined at runtime has a list of attributes as parameters. |
| auto verifier = [](function_ref<InFlightDiagnostic()> emitError, |
| ArrayRef<Attribute> args) { |
| ... |
| }; |
| |
| // The attribute parameters parser. |
| auto parser = [](DialectAsmParser &parser, |
| llvm::SmallVectorImpl<Attribute> &parsedParams) { |
| ... |
| }; |
| |
| // The attribute parameters printer. |
| auto printer =[](DialectAsmPrinter &printer, ArrayRef<Attribute> params) { |
| ... |
| }; |
| |
| std::unique_ptr<DynamicAttrDefinition> attrDef = |
| DynamicAttrDefinition::get(std::move(name), std::move(dialect), |
| std::move(verifier), std::move(printer), |
| std::move(parser)); |
| ``` |
| |
| If the printer and the parser are ommited, a default parser and printer is |
| generated with the format `!dialect.attrname<arg1, arg2, ..., argN>`. |
| |
| The attribute can then be registered by the `ExtensibleDialect`: |
| |
| ```c++ |
| dialect->registerDynamicAttr(std::move(typeDef)); |
| ``` |
| |
| ### Parsing attributes defined at runtime in an extensible dialect |
| |
| `parseAttribute` methods generated by TableGen can parse attributes defined at |
| runtime, though overriden `parseAttribute` methods need to add the necessary |
| support for them. |
| |
| ```c++ |
| Attribute MyDialect::parseAttribute(DialectAsmParser &parser, |
| Type type) const override { |
| ... |
| // The attribute name. |
| StringRef attrTag; |
| if (failed(parser.parseKeyword(&attrTag))) |
| return Attribute(); |
| |
| // Try to parse a dynamic attribute with 'attrTag' name. |
| Attribute dynAttr; |
| auto parseResult = parseOptionalDynamicAttr(attrTag, parser, dynAttr); |
| if (parseResult.has_value()) { |
| if (succeeded(*parseResult)) |
| return dynAttr; |
| return Attribute(); |
| } |
| ``` |
| |
| ### Using an attribute defined at runtime |
| |
| Similar to types, attributes defined at runtime are instances of `DynamicAttr`. |
| It is possible to get a dynamic attribute with `DynamicAttr::get` and |
| `ExtensibleDialect::lookupAttrDefinition`. |
| |
| ```c++ |
| auto attrDef = extensibleDialect->lookupAttrDefinition("my_dynamic_attr"); |
| ArrayRef<Attribute> params = ...; |
| auto attr = DynamicAttr::get(attrDef, params); |
| ``` |
| |
| It is also possible to cast an `Attribute` known to be defined at runtime to a |
| `DynamicAttr`. |
| |
| ```c++ |
| auto dynAttr = cast<DynamicAttr>(attr); |
| auto attrDef = dynAttr.getAttrDef(); |
| auto args = dynAttr.getParams(); |
| ``` |
| |
| ### Implementation Details of Extensible Dialects |
| |
| #### Extensible dialect |
| |
| The role of extensible dialects is to own the necessary data for defined |
| operations and types. They also contain the necessary accessors to easily |
| access them. |
| |
| In order to cast a `Dialect` back to an `ExtensibleDialect`, we implement the |
| `IsExtensibleDialect` interface to all `ExtensibleDialect`. The casting is done |
| by checking if the `Dialect` implements `IsExtensibleDialect` or not. |
| |
| #### Operation representation and registration |
| |
| Operations are represented in mlir using the `AbstractOperation` class. They are |
| registered in dialects the same way operations defined in C++ are registered, |
| which is by calling `AbstractOperation::insert`. |
| |
| The only difference is that a new `TypeID` needs to be created for each |
| operation, since operations are not represented by a C++ class. This is done |
| using a `TypeIDAllocator`, which can allocate a new unique `TypeID` at runtime. |
| |
| #### Type representation and registration |
| |
| Unlike operations, types need to define a C++ storage class that takes care of |
| type parameters. They also need to define another C++ class to access that |
| storage. `DynamicTypeStorage` defines the storage of types defined at runtime, |
| and `DynamicType` gives access to the storage, as well as defining useful |
| functions. A `DynamicTypeStorage` contains a list of `Attribute` type |
| parameters, as well as a pointer to the type definition. |
| |
| Types are registered using the `Dialect::addType` method, which expect a |
| `TypeID` that is generated using a `TypeIDAllocator`. The type uniquer also |
| register the type with the given `TypeID`. This mean that we can reuse our |
| single `DynamicType` with different `TypeID` to represent the different types |
| defined at runtime. |
| |
| Since the different types defined at runtime have different `TypeID`, it is not |
| possible to use `TypeID` to cast a `Type` into a `DynamicType`. Thus, similar to |
| `Dialect`, all `DynamicType` define a `IsDynamicTypeTrait`, so casting a `Type` |
| to a `DynamicType` boils down to querying the `IsDynamicTypeTrait` trait. |