| # 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. |
| |
| 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. |