xref: /llvm-project/mlir/docs/Tutorials/transform/Ch3.md (revision b33b91a21788d439f49d6db4e7224c20f740f1a7)
1# Chapter 3: More than Simple Transform Operations
2
3## Type Constraints and ApplyEach Trait
4
5A transform operation that applies to each payload operation individually and requires it to be of a specific kind is a repeated pattern. One can use Transform dialect types to specify the preconditions of the type. Specifically, we can change the expected operand type from the wide `TransformHandleTypeInterface` to the more narrow `Transform_ConcreteOp<"func.call">`. Furthermore, we use the `TransformEachOpTrait` trait to provide the skeleton implementation of the `apply` method that performs verification, iteration over payloads and result concatenation. The improved ODS op definition is as follows.
6
7```tablegen
8// In MyExtension.td.
9
10// Define the new operation. By convention, prefix its name with the name of the dialect extension, "my.". The full operation name will be further prefixed with "transform.".
11def ChangeCallTargetOp : Op<Transform_Dialect, "my.change_call_target",
12    // Indicate that the operation implements the required TransformOpInterface and
13    // MemoryEffectsOpInterface. Use the TransformEach trait to provide the
14    // implementation for TransformOpInterface.
15    [TransformOpInterface, TransformEachOpTrait,
16     DeclareOpInterfaceMethods<MemoryEffectsOpInterface>]> {
17  // Provide a brief and a full description. It is recommended that the latter describes
18  // the effects on the operands and how the operation processes various failure modes.
19  let summary = "Changes the callee of a call operation to the specified one";
20  let description = [{
21    For each `func.call` payload operation associated with the handle, changes its
22    callee to be the symbol whose name is provided as an attribute to this operation.
23
24    Generates a silenceable failure if the operand is associated with payload operations
25    that are not `func.call`.
26    Only reads the operand.
27  }];
28
29  // The arguments include the handle to the payload operations and the attribute that
30  // specifies the new callee. The handle must implement TransformHandleTypeInterface.
31  // We use a string attribute as the symbol may not exist in the transform IR so the
32  // verification may fail.
33  let arguments = (ins
34    Transform_ConcreteOpType<"func.call">:$call,
35    StrAttr:$new_target);
36
37  // The results are empty as the transformation does not produce any new payload.
38  let results = (outs);
39
40  // Provide nice syntax.
41  let assemblyFormat = "$call `,` $new_target attr-dict `:` type($call)";
42
43  // Declare the function implementing the interface for a single payload operation.
44  let extraClassDeclaration = [{
45    ::mlir::DiagnosedSilenceableFailure applyToOne(
46        ::mlir::transform::TransformRewriter &rewriter,
47        ::mlir::func::CallOp call,
48        ::mlir::transform::ApplyToEachResultList &results,
49        ::mlir::transform::TransformState &state);
50  }];
51}
52```
53
54Now, instead of defining the `apply` method with a loop, we can simply define a function that applies to an individual payload operation and the trait will take care of the rest.
55
56```c++
57::mlir::DiagnosedSilenceableFailure ChangeCallTargetOp::applyToOne(
58    ::mlir::transform::TransformRewriter &rewriter,
59    ::mlir::func::CallOp call,
60    ::mlir::transform::ApplyToEachResultList &results,
61    ::mlir::transform::TransformState &state) {
62  // Call the actual transformation function.
63  updateCallee(call, getNewTarget());
64  // Indicate success.
65  return DiagnosedSilenceableFailure::success();
66}
67```
68
69## Defining a Transform Type
70
71In addition to operations, the Transform dialect allows extensions to define and inject additional attributes and types. As we have seen above, transform types are used to specify constraints on the payload operations. Our call rewriting operation currently applies only to `func.call`. We may want to generalize it to apply to any payload operation that implements `CallOpInterface`, but the Transform dialect currently doesn’t provide a type that checks if a payload operation implements this interface. Let’s define it in our extension.
72
73Type definition is again identical to defining a dialect type with ODS.
74
75```tablegen
76// Transform dialect allows additional types to be defined and injected.
77def CallOpInterfaceHandle
78  : TypeDef<Transform_Dialect, "CallOpInterfaceHandle",
79      // The type must implement `TransformHandleTypeInterface`.
80      [DeclareTypeInterfaceMethods<TransformHandleTypeInterface>]> {
81
82  // The usual components of a type such as description, mnemonic and assembly format
83  // should be provided.
84  let summary = "handle to payload operations implementing CallOpInterface";
85  let mnemonic = "my.call_op_interface";
86  let assemblyFormat = "";
87}
88```
89
90We will omit the generation of declaration and definitions using Tablegen for brevity as it is identical to the regular case.
91
92To finalize the definition of a transform type, one must implement the interface methods.
93
94```c++
95// In MyExtension.cpp.
96
97// The interface declares this method to verify constraints this type has on
98// payload operations. It returns the now familiar tri-state result.
99mlir::DiagnosedSilenceableFailure
100mlir::transform::CallOpInterfaceHandleType::checkPayload(
101    // Location at which diagnostics should be emitted.
102    mlir::Location loc,
103    // List of payload operations that are about to be associated with the
104    // handle that has this type.
105    llvm::ArrayRef<mlir::Operation *> payload) const {
106
107  // All payload operations are expected to implement CallOpInterface, check this.
108  for (Operation *op : payload) {
109    if (llvm::isa<mlir::CallOpInterface>(op))
110      continue;
111
112    // By convention, these verifiers always emit a silenceable failure since they are
113    // checking a precondition.
114    DiagnosedSilenceableFailure diag = emitSilenceableError(loc)
115        << "expected the payload operation to implement CallOpInterface";
116    diag.attachNote(op->getLoc()) << "offending operation";
117    return diag;
118  }
119
120  // If everything is okay, return success.
121  return DiagnosedSilenceableFailure::success();
122}
123
124```
125
126Additional attributes and types need to be registered in the extension, next to operations.
127
128```c++
129// In MyExtension.cpp.
130
131void MyExtension::init() {
132  // ...
133
134  registerTypes<
135#define GET_TYPEDEF_LIST
136#include "MyExtensionTypes.cpp.inc"
137  >();
138}
139```
140
141This type is now directly available in the Transform dialect and can be used in operations.
142
143
144```mlir
145  // Cast to our new type.
146  %casted = transform.cast %call : !transform.any_op to !transform.my.call_op_interface
147  // Using our new operation.
148  transform.my.change_call_target %casted, "microkernel" : !transform.my.call_op_interface
149```
150
151## Operand Consumption
152
153As an exercise, let us modify the rewriting operation to consume the operand. This would be necessary, for example, if the transformation were to rewrite the `func.call` operation into a custom operation `my.mm4`. Since the operand handle is now consumed, the operation can return a new handle to the newly produced payload operation. Otherwise, the ODS definition of the transform operation remains unchanged.
154
155```tablegen
156// In MyExtension.td.
157
158// Define another transform operation.
159def CallToOp : Op<Transform_Dialect, "my.call_to_op",
160     // Indicate that the operation implements the required TransformOpInterface and
161     // MemoryEffectsOpInterface. Use the TransformEach trait to provide the
162     // implementation for TransformOpInterface.
163    [TransformOpInterface, TransformEachOpTrait,
164     DeclareOpInterfaceMethods<MemoryEffectsOpInterface>]> {
165  // Summary and description omitted for brevity.
166
167  // The argument is the handle to the payload operations.
168  let arguments = (ins CallOpInterfaceHandle:$call);
169
170  // The result is the handle to the payload operations produced during the
171  // transformation.
172  let results = (outs TransformHandleTypeInterface:$transformed);
173
174  // Provide nice syntax.
175  let assemblyFormat = "$call attr-dict `:` functional-type(inputs, outputs)";
176
177  // Declare the function implementing the interface for a single payload operation.
178  let extraClassDeclaration = [{
179    ::mlir::DiagnosedSilenceableFailure applyToOne(
180        ::mlir::transform::TransformRewriter &rewriter,
181        ::mlir::CallOpInterface call,
182        ::mlir::transform::ApplyToEachResultList &results,
183        ::mlir::transform::TransformState &state);
184  }];
185}
186```
187
188Now let’s look at the implementation of interface methods.
189
190```c++
191// In MyExtension.cpp.
192
193::mlir::DiagnosedSilenceableFailure CallToOp::applyToOne(
194    ::mlir::transform::TransformRewriter &rewriter,
195    ::mlir::CallOpInterface call,
196    ::mlir::transform::ApplyToEachResultList &results,
197    ::mlir::transform::TransformState &state) {
198  // Call the actual rewrite.
199  Operation *rewritten = rewriteToOp(call);
200
201  // Report an error if the rewriter produced a null pointer. Note that it may have
202  // irreversibly modified the payload IR, so we produce a definite failure.
203  if (!rewritten) {
204    return emitDefiniteError() << "failed to rewrite call to operation";
205  }
206
207  // On success, push the resulting operation into the result list. The list is expected
208  // to contain exactly one entity per result and per application. The handles will be
209  // associated with lists of the respective values produced by each application.
210  results.push_back(rewritten);
211
212  // If everything is fine, return success.
213  return DiagnosedSilenceableFailure::success();
214}
215
216void CallToOp::getEffects(
217    ::llvm::SmallVectorImpl<::mlir::MemoryEffects::EffectInstance> &effects) {
218  // Indicate using side effects that the operand handle is consumed, and the
219  // result handle is produced.
220  consumesHandle(getCall(), effects);
221  producesHandle(getTransformed(), effects);
222
223  // Indicate that the payload IR is modified.
224  modifiesPayload(effects);
225}
226```
227
228The overall flow of these implementations is similar to the previous one. The application also needs to specify the resulting entities that are going to be associated with the handles it produces. Operations are required to specify the entities to associate with _all_ results on success, even if the list is empty. An assertion will be triggered if it is not the case. In case of failure, the interpreter will automatically associate all results that are not yet defined with empty lists.
229
230Note that since `applyToOne` always expects one payload entity to be associated with each result handle in each application, it cannot be used to return handles associated with empty lists for non-empty operand handles. Instead, one would use `apply` directly.
231
232```c++
233::mlir::DiagnosedSilenceableFailure SomeOtherOp::apply(
234    ::mlir::transform::TransformRewriter &rewriter,
235    ::mlir::transform::TransformResults &results,
236    ::mlir::transform::TransformState &state) {
237  // ...
238
239  // Associate the result `transformed` with an empty list of payload operations.
240  results.set(cast<OpResult>(getTransformed()), {});
241  return DiagnosedSilenceableFailure::success();
242}
243```
244
245## Memory Effects Traits
246
247Some common memory effect patterns are also available as traits to minimize the boilerplate.
248
249*   `FunctionalStyleTransformOpTrait` indicates that all handle-typed operands are consumed, all results are produced, and the payload IR is modified.
250*   `NavigationTransformOpTrait` indicates that all handle-typed operands are only read, all results are produced, and the payload IR is only read.
251
252Using these traits removes the need to declare or define the methods of the `MemoryEffectsOpInterface`.
253
254```tablegen
255// In MyExtension.td.
256
257// Define another transform operation.
258def CallToOp : Op<Transform_Dialect, "my.call_to_op",
259     // Indicate that the operation implements the required TransformOpInterface.
260     // Use the TransformEach trait to provide implementation of this interface.
261    [TransformOpInterface, TransformEachOpTrait,
262     // Indicate that the operation implements the required MemoryEffectsOpInterface.
263     // Use the FunctionalStyle trait to provide the implementation for this interface.
264     MemoryEffectsOpInterface, FunctionalStyleTransformOpTrait]> {
265  // Summary and description omitted for brevity.
266
267  // The argument is the handle to the payload operations.
268  let arguments = (ins CallOpInterfaceHandle:$call);
269
270  // The result is the handle to the payload operations produced during the
271  // transformation.
272  let results = (outs TransformHandleTypeInterface:$transformed);
273
274  // Provide nice syntax.
275  let assemblyFormat = "$call attr-dict `:` functional-type(operands, results)";
276
277  // Declare the function implementing the interface for a single payload operation.
278  let extraClassDeclaration = [{
279    ::mlir::DiagnosedSilenceableFailure applyToOne(
280        ::mlir::transform::TransformRewriter &rewriter,
281        ::mlir::CallOpInterface call,
282        ::mlir::transform::ApplyToEachResultList &results,
283        ::mlir::transform::TransformState &state);
284  }];
285}
286```
287
288## Appendix: Autogenerated Documentation
289
290[include "Tutorials/transform/MyExtensionCh3.md"]
291
292