| # 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). | |
| ### Strict Property Assembly Formats | |
| Declarative assembly formats for property-backed operations must account for | |
| all inherent attributes and properties by default. An operation format must | |
| either bind every inherent attribute and property directly in the format or | |
| include the `prop-dict` directive. Generated parsers also reject inherent | |
| attributes that arrive through `attr-dict`, so `attr-dict` only carries | |
| discardable attributes for these formats. | |
| The `useStrictPropertiesInAssemblyFormat` field is deprecated. Setting it to | |
| `0` temporarily opts a dialect into legacy behavior, allowing inherent | |
| attributes to mix with discardable attributes in `attr-dict`. Dialects using | |
| this setting should migrate their formats to bind inherent attributes directly | |
| or use `prop-dict`. | |
| See the [declarative assembly format](Operations.md/#declarative-assembly-format) | |
| documentation for the corresponding format requirements. | |
| ### 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. |