| # Dialect Conversion | |
| This document describes a framework in MLIR in which to perform operation | |
| conversions between, and within dialects. This framework allows for transforming | |
| illegal operations to those supported by a provided conversion target, via a set | |
| of pattern-based operation rewriting patterns. | |
| The dialect conversion framework consists of the following components: | |
| * A [Conversion Target](#conversion-target) | |
| * A set of [Rewrite Patterns](#rewrite-pattern-specification) | |
| * A [Type Converter](#type-conversion) (Optional) | |
| [TOC] | |
| ## Modes of Conversion | |
| When applying a conversion to a set of operations, there are several different | |
| conversion modes that may be selected from: | |
| * Partial Conversion | |
| - A partial conversion will legalize as many operations to the target as | |
| possible, but will allow pre-existing operations that were not | |
| explicitly marked as "illegal" to remain unconverted. This allows for | |
| partially lowering parts of the input in the presence of unknown | |
| operations. | |
| - A partial conversion can be applied via `applyPartialConversion`. | |
| * Full Conversion | |
| - A full conversion legalizes all input operations, and is only successful | |
| if all operations are properly legalized to the given conversion target. | |
| This ensures that only known operations will exist after the conversion | |
| process. | |
| - A full conversion can be applied via `applyFullConversion`. | |
| * Analysis Conversion | |
| - An analysis conversion will analyze which operations are legalizable to | |
| the given conversion target if a conversion were to be applied. This is | |
| done by performing a 'partial' conversion and recording which operations | |
| would have been successfully converted if successful. Note that no | |
| rewrites, or transformations, are actually applied to the input | |
| operations. | |
| - An analysis conversion can be applied via `applyAnalysisConversion`. | |
| In all cases, the framework walks the operations in preorder, examining an op | |
| before the ops in any regions it has. | |
| ## Conversion Target | |
| The conversion target is a formal definition of what is considered to be legal | |
| during the conversion process. The final operations generated by the conversion | |
| framework must be marked as legal on the `ConversionTarget` for the rewrite to | |
| be a success. Depending on the conversion mode, existing operations need not | |
| always be legal. Operations and dialects may be marked with any of the provided | |
| legality actions below: | |
| * Legal | |
| - This action signals that every instance of a given operation is legal, | |
| i.e. any combination of attributes, operands, types, etc. are valid. | |
| * Dynamic | |
| - This action signals that only some instances of a given operation are | |
| legal. This allows for defining fine-tune constraints, e.g. saying that | |
| `arith.addi` is only legal when operating on 32-bit integers. | |
| * Illegal | |
| - This action signals that no instance of a given operation is legal. | |
| Operations marked as "illegal" must always be converted for the | |
| conversion to be successful. This action also allows for selectively | |
| marking specific operations as illegal in an otherwise legal dialect. | |
| Operations and dialects that are neither explicitly marked legal nor illegal are | |
| separate from the above ("unknown" operations) and are treated differently, for | |
| example, for the purposes of partial conversion as mentioned above. | |
| An example conversion target is shown below: | |
| ```c++ | |
| struct MyTarget : public ConversionTarget { | |
| MyTarget(MLIRContext &ctx) : ConversionTarget(ctx) { | |
| //-------------------------------------------------------------------------- | |
| // Marking an operation as Legal: | |
| /// Mark all operations within the LLVM dialect are legal. | |
| addLegalDialect<LLVMDialect>(); | |
| /// Mark `arith.constant` op is always legal on this target. | |
| addLegalOp<arith::ConstantOp>(); | |
| //-------------------------------------------------------------------------- | |
| // Marking an operation as dynamically legal. | |
| /// Mark all operations within Affine dialect have dynamic legality | |
| /// constraints. | |
| addDynamicallyLegalDialect<affine::AffineDialect>( | |
| [](Operation *op) { ... }); | |
| /// Mark `func.return` as dynamically legal, but provide a specific legality | |
| /// callback. | |
| addDynamicallyLegalOp<func::ReturnOp>([](func::ReturnOp op) { ... }); | |
| /// Treat unknown operations, i.e. those without a legalization action | |
| /// directly set, as dynamically legal. | |
| markUnknownOpDynamicallyLegal([](Operation *op) { ... }); | |
| //-------------------------------------------------------------------------- | |
| // Marking an operation as illegal. | |
| /// All operations within the GPU dialect are illegal. | |
| addIllegalDialect<GPUDialect>(); | |
| /// Mark `cf.br` and `cf.cond_br` as illegal. | |
| addIllegalOp<cf::BranchOp, cf::CondBranchOp>(); | |
| } | |
| /// Implement the default legalization handler to handle operations marked as | |
| /// dynamically legal that were not provided with an explicit handler. | |
| bool isDynamicallyLegal(Operation *op) override { ... } | |
| }; | |
| ``` | |
| ### Recursive Legality | |
| In some cases, it may be desirable to mark entire regions as legal. This | |
| provides an additional granularity of context to the concept of "legal". If an | |
| operation is marked recursively legal, either statically or dynamically, then | |
| all of the operations nested within are also considered legal even if they would | |
| otherwise be considered "illegal". An operation can be marked via | |
| `markOpRecursivelyLegal<>`: | |
| ```c++ | |
| ConversionTarget &target = ...; | |
| /// The operation must first be marked as `Legal` or `Dynamic`. | |
| target.addLegalOp<MyOp>(...); | |
| target.addDynamicallyLegalOp<MySecondOp>(...); | |
| /// Mark the operation as always recursively legal. | |
| target.markOpRecursivelyLegal<MyOp>(); | |
| /// Mark optionally with a callback to allow selective marking. | |
| target.markOpRecursivelyLegal<MyOp, MySecondOp>([](Operation *op) { ... }); | |
| /// Mark optionally with a callback to allow selective marking. | |
| target.markOpRecursivelyLegal<MyOp>([](MyOp op) { ... }); | |
| ``` | |
| ## Rewrite Pattern Specification | |
| After the conversion target has been defined, a set of legalization patterns | |
| must be provided to transform illegal operations into legal ones. The patterns | |
| supplied here have the same structure and similar restrictions as those | |
| described in the main [Pattern](PatternRewriter.md) documentation. The patterns | |
| provided do not need to generate operations that are directly legal on the | |
| target. The framework will automatically build a graph of conversions to convert | |
| non-legal operations into a set of legal ones. | |
| As an example, say you define a target that supports one operation: `foo.add`. | |
| When providing the following patterns: [`bar.add` -> `baz.add`, `baz.add` -> | |
| `foo.add`], the framework will automatically detect that it can legalize | |
| `bar.add` -> `foo.add` even though a direct conversion does not exist. This | |
| means that you don’t have to define a direct legalization pattern for `bar.add` | |
| -> `foo.add`. | |
| ### Conversion Patterns | |
| Along with the general `RewritePattern` classes, the conversion framework | |
| provides a special type of rewrite pattern that can be used when a pattern | |
| relies on interacting with constructs specific to the conversion process, the | |
| `ConversionPattern`. | |
| #### Remapped Operands / Adaptor | |
| Conversion patterns have an additional `operands` / `adaptor` argument for the | |
| `matchAndRewrite` method. These operands correspond to the most recent | |
| replacement values of the respective operands of the matched operation. | |
| ```c++ | |
| struct MyConversionPattern : public ConversionPattern { | |
| /// The `matchAndRewrite` hooks on ConversionPatterns take an additional | |
| /// `operands` parameter, containing the remapped operands of the original | |
| /// operation. | |
| virtual LogicalResult | |
| matchAndRewrite(Operation *op, ArrayRef<Value> operands, | |
| ConversionPatternRewriter &rewriter) const; | |
| }; | |
| ``` | |
| Example: | |
| ```mlir | |
| %0 = "test.foo"() : () -> i1 // matched by pattern A | |
| "test.bar"(%0) : (i1) -> () // matched by pattern B | |
| ``` | |
| Let's assume that the two patterns are applied back-to-back: first, pattern A | |
| replaces `"test.foo"` with `"test.qux"`, an op that has a different result | |
| type. The dialect conversion infrastructure has special support for such | |
| type-changing IR modifications. | |
| ```mlir | |
| %0 = "test.qux"() : () -> i2 | |
| %r = builtin.unrealized_conversion_cast %0 : i2 to i1 | |
| "test.bar"(%r) : (i1) -> () | |
| ``` | |
| Simply swapping out the operand of `"test.bar"` during the `replaceOp` call | |
| would be unsafe, because that would change the type of operand and, therefore, | |
| potentially the semantics of the operation. Instead, the dialect conversion | |
| driver (conceptually) inserts a `builtin.unrealized_conversion_cast` op that | |
| connects the newly created `"test.qux"` op with the `"test.bar"` op, without | |
| changing the types of the latter one. | |
| Now, the second pattern B is applied. The `operands` argument contains an SSA | |
| value with the most recent replacement type (`%0` with type `i2`), whereas | |
| querying the operand from the matched op still returns an SSA value with the | |
| original operand type `i1`. | |
| Note: If the conversion pattern is instantiated with a type converter, the | |
| `operands` argument contains SSA values whose types match the legalized operand | |
| types as per the type converter. See [Type Safety](#type-safety) for more | |
| details. | |
| Note: The dialect conversion framework does not guarantee the presence of any | |
| particular value in the `operands` argument. The only thing that's guaranteed | |
| is the type of the `operands` SSA values. E.g., instead of the actual | |
| replacement values supplied to a `replaceOp` API call, `operands` may contain | |
| results of transitory `builtin.unrealized_conversion_cast` ops that were | |
| inserted by the conversion driver but typically fold away again throughout the | |
| conversion process. | |
| #### Immediate vs. Delayed IR Modification | |
| The dialect conversion driver can operate in two modes: (a) rollback mode | |
| (default) and (b) no-rollback mode. This can be controlled by | |
| `ConversionConfig::allowPatternRollback`. When running in rollback mode, the | |
| driver is able backtrack and roll back already applied patterns when the | |
| current legalization path (sequence of pattern applications) gets stuck with | |
| unlegalizable operations. | |
| When running in no-rollback mode, all IR modifications such as op replacement, | |
| op erasure, op insertion or in-place op modification are applied immediately. | |
| When running in rollback mode, certain IR modifications are delayed to the end | |
| of the conversion process. For example, a `ConversionPatternRewriter::eraseOp` | |
| API call does not immediately erase the op, but just marks it for erasure. The | |
| op will stay visible to patterns and IR traversals throughout the conversion | |
| process. As another example, `replaceOp` and `replaceAllUsesWith` does not | |
| immediately update users of the original SSA values. This step is also delayed | |
| to the end of the conversion process. | |
| Delaying certain IR modifications has two benefits: (1) pattern rollback is | |
| simpler because fewer IR modifications must be rolled back, (2) pointers of | |
| erased operations / blocks are preserved upon rollback, and (3) patterns can | |
| still access/traverse the original IR to some degree. However, additional | |
| bookkeeping in the form of complex internal C++ data structures is required to | |
| support pattern rollback. Running in rollback mode has a significant toll on | |
| compilation time, is error-prone and makes debugging conversion passes more | |
| complicated. Therefore, programmers are encouraged to run in no-rollback mode | |
| when possible. | |
| The following table gives an overview of which IR changes are applied in a | |
| delayed fasion in rollback mode. | |
| | Type | Rollback Mode | No-rollback Mode | | |
| | ------------------------------------------------------- | ----------------- | ---------------- | | |
| | Op Insertion / Movement (`create`/`insert`) | Immediate | Immediate | | |
| | Op Replacement (`replaceOp`) | Delayed | Immediate | | |
| | Op Erasure (`eraseOp`) | Delayed | Immediate | | |
| | Op Modification (`modifyOpInPlace`) | Immediate | Immediate | | |
| | Value Replacement (`replaceAllUsesWith`) | Delayed | Immediate | | |
| | Block Insertion (`createBlock`) | Immediate | Immediate | | |
| | Block Replacement | Not supported | Not supported | | |
| | Block Erasure | Partly delayed | Immediate | | |
| | Block Signature Conversion (`applySignatureConversion`) | Partially delayed | Immediate | | |
| | Region / Block Inlining (`inlineBlockBefore`, etc.) | Partially delayed | Immediate | | |
| Value replacement is delayed and has different semantics in rollback mode: | |
| Since the actual replacement is delayed to the end of the conversion process, | |
| additional uses of the replaced value can be created after the | |
| `replaceAllUsesWith` call. Those uses will also be replaced at the end of the | |
| conversion process. | |
| Block replacement is not supported in either mode, because the rewriter | |
| infrastructure currently has no API for replacing blocks: there is no overload | |
| of `replaceAllUsesWith` that accepts `Block *`. | |
| Block erasure is partly delayed in rollback mode: the block is detached from | |
| the IR graph, but not memory for the block is not released until the end of the | |
| conversion process. This mechanism ensures that block pointers do not change | |
| when a block erasure is rolled back. | |
| Block signature conversion is a combination of block insertion, op insertion, | |
| value replacement and block erasure. In rollback mode, the first two steps are | |
| immediate, but the last two steps are delayed. | |
| Region / block inlining is a combination of block / op insertion and | |
| (optionally) value replacement. In rollback mode, the insertion steps are | |
| immediate, but the replacement step is delayed. | |
| Note: When running in rollback mode, the conversion driver inserts fewer | |
| transitory `builtin.unrealized_conversion_cast` ops. Such ops are needed less | |
| frequently because certain IR modifications are delayed, making it unnecessary | |
| to connect old (not yet rewritten) and new (already rewritten) IR in a | |
| type-safe way. This has a negative effect on the debugging experience: when | |
| dumping IR throughout the conversion process, users see a mixture of old and | |
| new IR, but the way they are connected is not always visibile in the IR. Some | |
| of that information is stored in internal C++ data structures that is not | |
| visibile during an IR dump. | |
| #### Type Safety | |
| The types of the remapped operands provided to a conversion pattern (through | |
| the adaptor or `ArrayRef` of operands) depend on type conversion rules. | |
| If the pattern was initialized with a [type converter](#type-converter), the | |
| conversion driver passes values whose types match the legalized types of the | |
| operands of the matched operation as per the type converter. To that end, the | |
| conversion driver may insert target materializations to convert the most | |
| recently mapped values to the expected legalized types. The driver tries to | |
| reuse existing materializations on a best-effort basis, but this is not | |
| guaranteed by the infrastructure. If the operand types of the matched op could | |
| not be legalized, the pattern fails to apply before the `matchAndRewrite` hook | |
| is invoked. | |
| Example: | |
| ```c++ | |
| // Type converter that converts all FloatTypes to IntegerTypes. | |
| TypeConverter converter; | |
| converter.addConversion([](FloatType t) { | |
| return IntegerType::get(t.getContext(), t.getWidth()); | |
| }); | |
| // Assuming that `MyConversionPattern` was initialized with `converter`. | |
| struct MyConversionPattern : public ConversionPattern { | |
| virtual LogicalResult | |
| matchAndRewrite(Operation *op, ArrayRef<Value> operands, /* ... */) const { | |
| // ^^^^^^^^ | |
| // If `op` has a FloatType operand, the respective value in `operands` | |
| // is guaranteed to have the legalized IntegerType. If another pattern | |
| // previously replaced the operand SSA value with an SSA value of the | |
| // legalized type (via "replaceOp" or "applySignatureConversion"), you | |
| // will get that SSA value directly (unless the replacement value was | |
| // also replaced). Otherwise, you will get a materialization to the | |
| // legalized type. | |
| ``` | |
| If the pattern was initialized without a type converter, the conversion driver | |
| passes the most recently mapped values to the pattern, excluding any | |
| materializations. If a value with the same type as the original operand is | |
| desired, users can directly take the respective operand from the matched | |
| operation. | |
| Example: When initializing the pattern from the above example without a type | |
| converter, `operands` contains the most recent replacement values, regardless | |
| of their types. | |
| Note: When running without a type converter, materializations are intentionally | |
| excluded from the lookup process because their presence may depend on other | |
| patterns. Passing materializations would make the conversion infrastructure | |
| fragile and unpredictable. Moreover, there could be multiple materializations | |
| to different types. (This can be the case when multiple patterns are running | |
| with different type converters.) In such a case, it would be unclear which | |
| materialization to pass. | |
| The above rules ensure that patterns do not have to explicitly ensure type | |
| safety, or sanitize the types of the incoming remapped operands. More | |
| information on type conversion is detailed in the | |
| [dedicated section](#type-conversion) below. | |
| ## Type Conversion | |
| It is sometimes necessary as part of a conversion to convert the set types of | |
| being operated on. In these cases, a `TypeConverter` object may be defined that | |
| details how types should be converted when interfacing with a pattern. A | |
| `TypeConverter` may be used to convert the signatures of block arguments and | |
| regions, to define the expected inputs types of the pattern, and to reconcile | |
| type differences in general. | |
| ### Type Converter | |
| The `TypeConverter` contains several hooks for detailing how to convert types, | |
| and how to materialize conversions between types in various situations. The two | |
| main aspects of the `TypeConverter` are conversion and materialization. | |
| A `conversion` describes how a given source `Type` should be converted to N | |
| target types. If the source type is converted to itself, we say it is a "legal" | |
| type. Type conversions are specified via the `addConversion` method described | |
| below. | |
| There are two kind of conversion functions: context-aware and context-unaware | |
| conversions. A context-unaware conversion function converts a `Type` into a | |
| `Type`. A context-aware conversion function converts a `Value` into a type. The | |
| latter allows users to customize type conversion rules based on the IR. | |
| Note: context-aware type conversion functions impact the ability of the | |
| framework to cache the conversion result. In the absence of a context-aware | |
| conversion, all context-free type conversions can be cached. Otherwise only the | |
| context-free conversions added after a context-aware type conversion can be | |
| cached (conversions are applied in reverse order). | |
| As such it is advised to add context-aware conversions as early as possible in | |
| the sequence of `addConversion` calls (so that they apply last). | |
| A `materialization` describes how a list of values should be converted to a | |
| list of values with specific types. An important distinction from a | |
| `conversion` is that a `materialization` can produce IR, whereas a `conversion` | |
| cannot. These materializations are used by the conversion framework to ensure | |
| type safety during the conversion process. There are several types of | |
| materializations depending on the situation. | |
| * Source Materialization | |
| - A source materialization is used when a value was replaced with a value | |
| of a different type, but there are still users that expects the original | |
| ("source") type at the end of the conversion process. A source | |
| materialization converts the replacement value back to the source type. | |
| - This materialization is used in the following situations: | |
| * When a block argument has been converted to a different type, but | |
| the original argument still has users that will remain live after | |
| the conversion process has finished. | |
| * When a block argument has been dropped, but the argument still has | |
| users that will remain live after the conversion process has | |
| finished. | |
| * When the result type of an operation has been converted to a | |
| different type, but the original result still has users that will | |
| remain live after the conversion process is finished. | |
| * Target Materialization | |
| - A target materialization converts a value to the type that is expected | |
| by a conversion pattern according to its type converter. | |
| - A target materialization is used when a pattern expects the remapped | |
| operands to be of a certain set of types, but the original input | |
| operands have either not been replaced or been replaced with values of | |
| a different type. | |
| If a converted value is used by an operation that isn't converted, it needs a | |
| conversion back to the `source` type, hence source materialization; if an | |
| unconverted value is used by an operation that is being converted, it needs | |
| conversion to the `target` type, hence target materialization. | |
| As noted above, the conversion process guarantees that the type contract of the | |
| IR is preserved during the conversion. This means that the types of value uses | |
| will not implicitly change during the conversion process. When the type of a | |
| value definition, either block argument or operation result, is being changed, | |
| the users of that definition must also be updated during the conversion process. | |
| If they aren't, a type conversion must be materialized to ensure that a value of | |
| the expected type is still present within the IR. If a materialization is | |
| required, but cannot be performed, the entire conversion process fails. | |
| Several of the available hooks are detailed below: | |
| ```c++ | |
| class TypeConverter { | |
| public: | |
| /// Register a conversion function. A conversion function must be convertible | |
| /// to any of the following forms (where `T` is `Value` or a class derived | |
| /// from `Type`, including `Type` itself): | |
| /// | |
| /// * std::optional<Type>(T) | |
| /// - This form represents a 1-1 type conversion. It should return nullptr | |
| /// or `std::nullopt` to signify failure. If `std::nullopt` is returned, | |
| /// the converter is allowed to try another conversion function to | |
| /// perform the conversion. | |
| /// * std::optional<LogicalResult>(T, SmallVectorImpl<Type> &) | |
| /// - This form represents a 1-N type conversion. It should return | |
| /// `failure` or `std::nullopt` to signify a failed conversion. If the | |
| /// new set of types is empty, the type is removed and any usages of the | |
| /// existing value are expected to be removed during conversion. If | |
| /// `std::nullopt` is returned, the converter is allowed to try another | |
| /// conversion function to perform the conversion. | |
| /// | |
| /// Conversion functions that accept `Value` as the first argument are | |
| /// context-aware. I.e., they can take into account IR when converting the | |
| /// type of the given value. Context-unaware conversion functions accept | |
| /// `Type` or a derived class as the first argument. | |
| /// | |
| /// Note: Context-unaware conversions are cached, but context-aware | |
| /// conversions are not. | |
| /// | |
| /// Note: When attempting to convert a type, e.g. via 'convertType', the | |
| /// mostly recently added conversions will be invoked first. | |
| template <typename FnT, | |
| typename T = typename llvm::function_traits<FnT>::template arg_t<0>> | |
| void addConversion(FnT &&callback) { | |
| registerConversion(wrapCallback<T>(std::forward<FnT>(callback))); | |
| } | |
| /// All of the following materializations require function objects that are | |
| /// convertible to the following form: | |
| /// `std::optional<Value>(OpBuilder &, T, ValueRange, Location)`, | |
| /// where `T` is any subclass of `Type`. This function is responsible for | |
| /// creating an operation, using the OpBuilder and Location provided, that | |
| /// "casts" a range of values into a single value of the given type `T`. It | |
| /// must return a Value of the converted type on success, an `std::nullopt` if | |
| /// it failed but other materialization can be attempted, and `nullptr` on | |
| /// unrecoverable failure. It will only be called for (sub)types of `T`. | |
| /// Materialization functions must be provided when a type conversion may | |
| /// persist after the conversion has finished. | |
| /// This method registers a materialization that will be called when | |
| /// converting a replacement value back to its original source type. | |
| /// This is used when some uses of the original value persist beyond the main | |
| /// conversion. | |
| template <typename FnT, | |
| typename T = typename llvm::function_traits<FnT>::template arg_t<1>> | |
| void addSourceMaterialization(FnT &&callback) { | |
| sourceMaterializations.emplace_back( | |
| wrapSourceMaterialization<T>(std::forward<FnT>(callback))); | |
| } | |
| /// This method registers a materialization that will be called when | |
| /// converting a value to a target type according to a pattern's type | |
| /// converter. | |
| /// | |
| /// Note: Target materializations can optionally inspect the "original" | |
| /// type. This type may be different from the type of the input value. | |
| /// For example, let's assume that a conversion pattern "P1" replaced an SSA | |
| /// value "v1" (type "t1") with "v2" (type "t2"). Then a different conversion | |
| /// pattern "P2" matches an op that has "v1" as an operand. Let's furthermore | |
| /// assume that "P2" determines that the converted target type of "t1" is | |
| /// "t3", which may be different from "t2". In this example, the target | |
| /// materialization will be invoked with: outputType = "t3", inputs = "v2", | |
| /// originalType = "t1". Note that the original type "t1" cannot be recovered | |
| /// from just "t3" and "v2"; that's why the originalType parameter exists. | |
| /// | |
| /// Note: During a 1:N conversion, the result types can be a TypeRange. In | |
| /// that case the materialization produces a SmallVector<Value>. | |
| template <typename FnT, | |
| typename T = typename llvm::function_traits<FnT>::template arg_t<1>> | |
| void addTargetMaterialization(FnT &&callback) { | |
| targetMaterializations.emplace_back( | |
| wrapTargetMaterialization<T>(std::forward<FnT>(callback))); | |
| } | |
| }; | |
| ``` | |
| Materializations through the type converter are optional. If the | |
| `ConversionConfig::buildMaterializations` flag is set to "false", the dialect | |
| conversion driver builds an `unrealized_conversion_cast` op instead of calling | |
| the respective type converter callback whenever a materialization is required. | |
| ### Region Signature Conversion | |
| From the perspective of type conversion, the types of block arguments are a bit | |
| special. Throughout the conversion process, blocks may move between regions of | |
| different operations. Given this, the conversion of the types for blocks must be | |
| done explicitly via a conversion pattern. | |
| To convert the types of block arguments within a Region, a custom hook on the | |
| `ConversionPatternRewriter` must be invoked; `convertRegionTypes`. This hook | |
| uses a provided type converter to apply type conversions to all blocks of a | |
| given region. This hook also takes an optional | |
| `TypeConverter::SignatureConversion` parameter that applies a custom conversion | |
| to the entry block of the region. The types of the entry block arguments are | |
| often tied semantically to the operation, e.g., `func::FuncOp`, `AffineForOp`, | |
| etc. | |
| To convert the signature of just one given block, the `applySignatureConversion` | |
| hook can be used. Note that `applySignatureConversion` replaces and erases the | |
| original block, so each block's signature can be converted at most once per | |
| conversion run. | |
| A signature conversion, `TypeConverter::SignatureConversion`, can be built | |
| programmatically: | |
| ```c++ | |
| class SignatureConversion { | |
| public: | |
| /// Remap an input of the original signature with a new set of types. The | |
| /// new types are appended to the new signature conversion. | |
| void addInputs(unsigned origInputNo, ArrayRef<Type> types); | |
| /// Append new input types to the signature conversion, this should only be | |
| /// used if the new types are not intended to remap an existing input. | |
| void addInputs(ArrayRef<Type> types); | |
| /// Remap an input of the original signature with a range of types in the | |
| /// new signature. | |
| void remapInput(unsigned origInputNo, unsigned newInputNo, | |
| unsigned newInputCount = 1); | |
| /// Remap an input of the original signature to another `replacement` | |
| /// value. This drops the original argument. | |
| void remapInput(unsigned origInputNo, Value replacement); | |
| }; | |
| ``` | |
| The `TypeConverter` provides several default utilities for signature conversion | |
| and legality checking: | |
| `convertSignatureArgs`/`convertBlockSignature`/`isLegal(Region *|Type)`. | |
| ## Debugging | |
| To debug the execution of the dialect conversion framework, | |
| `-debug-only=dialect-conversion` may be used. This command line flag activates | |
| LLVM's debug logging infrastructure solely for the conversion framework. The | |
| output is formatted as a tree structure, mirroring the structure of the | |
| conversion process. This output contains all of the actions performed by the | |
| rewriter, how generated operations get legalized, and why they fail. | |
| Example output is shown below: | |
| ``` | |
| //===-------------------------------------------===// | |
| Legalizing operation : 'func.return'(0x608000002e20) { | |
| "func.return"() : () -> () | |
| * Fold { | |
| } -> FAILURE : unable to fold | |
| * Pattern : 'func.return -> ()' { | |
| ** Insert : 'spirv.Return'(0x6070000453e0) | |
| ** Replace : 'func.return'(0x608000002e20) | |
| //===-------------------------------------------===// | |
| Legalizing operation : 'spirv.Return'(0x6070000453e0) { | |
| "spirv.Return"() : () -> () | |
| } -> SUCCESS : operation marked legal by the target | |
| //===-------------------------------------------===// | |
| } -> SUCCESS : pattern applied successfully | |
| } -> SUCCESS | |
| //===-------------------------------------------===// | |
| ``` | |
| This output is describing the legalization of an `func.return` operation. We | |
| first try to legalize by folding the operation, but that is unsuccessful for | |
| `func.return`. From there, a pattern is applied that replaces the `func.return` | |
| with a `spirv.Return`. The newly generated `spirv.Return` is then processed for | |
| legalization, but is found to already legal as per the target. |