Best practices for op types in the transform dialect

I see that most (?) transform ops specify their operands and results as TransformHandleTypeInterface (or derivates like Optional). This even seems to be the case if only one concrete op can occur. However, a few ops are more specific and give a specific Transform_ConcreteOpType.

One example are the ops in, where most use TransformHandleTypeInterface, including TileToForallOp, whose forall_op output is always an scf.forall, except LowerPackOp and LowerUnpackOp, which use Transform_ConcreteOpType.

What are the best practices and/or recommendations for this choice and is there a plan to remove this inconsistency in the long term?

We have the following documented in the second-to-last paragraph of overview: Transform Dialect - MLIR

By convention, Transform dialect operations are expected to indicate narrow preconditions for their operands by enforcing operand type constraints in the their definitions and verifiers. On the contrary, operations are expected to have few constraints on their results. Specific instances of a transform operation can then be created with a more restricted result type than the constraint in the operation (e.g., the “find” operation only constrains the result type to be a transform IR type while its concrete instance can have a type with stricter constraints such as implementing the “tilable” interface). The verification will then happen at transform execution time. This approach allows one to capture payload IR operation properties in the transform IR without resorting to excessive use of type casts or coupling dialect extensions between themselves. It is a trade-off between verbosity/complexity and static hardening, which can be revised in the future.

Taking the example of TileToForallOp, the result is indeed always a scf.forall, but the users of this result may want a different type. For example, we may eventually introduce something like !transform.interface.loop_like and have loop transformations request handle of this type specifically to guarantee their inputs have the interface. It looks preferable then to have TileToForallOp to produce a value of !transform.interface.loop_like directly than putting type casts everywhere.

An argument can be made for swapping the narrow/wide direction. I am also open to ideas that make use of types helpful without making it annoying due to casts.

Thanks @ftynse for the reply! It’s high time that I look at the detailed documentation closer – I had skipped over it when I first bumped into it but forgot to back after working through the introductory tutorial.

For what I can tell, the design you and the documentation describe makes sense. However, the Lower(Unpack|Pack)Ops from linalg don’t follow it: they also specify ConcreteOpTypes in their result. Is that an intentional deviation or something that should be fixed?

I cooked up some regexes and there don’t seem to be too many other deviations. The only ones I found are:

  • EmptyTensorToAllocTensorOp, which has a ConcreteType.
  • MatchStructuredOperandOp, which returns an Optional<AnyTypeOf<[TransformAnyHandle,Transform_AffineMapParamType]>>. Since that contains TransformAnyHandle, I guess this is fine?
  • BufferizeToAllocationOp, which returns an AnyOpType and an AnyValue. Why? What’s the difference to/advantage over returning handles?