RFC: separable attribute/type interfaces

Currently, attribute and type interfaces are specified as base classes (with extra indirection steps) of the attribute or type that implements them. Adding an interface to an attribute or a type required to modify the definition of the attribute or type, which is not always desirable or possible. Adding interfaces to attributes or types of another dialect creates all sorts of layering problems. The canonical example is wanting to add interfaces to builtin types, which is only possible by making the interface itself builtin. This does not scale well.

I propose a mechanism for separable attribute and type interfaces: the interface implementation for a particular attribute or type class can be defined separately from the attribute or type class itself. It can then be attached to the attribute or the class within a given context. This mechanism is similar to and reuses pieces of the dialect fallback for op interfaces. It is also a counterpart to the delayed dialect interface registration that we already have and use to reduce the library interdependence.

A high-level sketch of the implementation is as follows: the interface definition contains a FallbackModel class, which declares interface methods that additionally take the attribute or type as leading argument.

struct InterfaceTraits {
  class Concept {
    virtual void *func(args...);
  };
  template <typename Concrete>
  class Model : public Concept {
    void func(args...) {}
  }
  template <typename Concrete>
  class FallbackModel : public Concept {
    void func(Attr/Type pseudoThis, args...);
  }
};

ODS already generates FallbackModel for all interfaces to support the dialect fallback mechanism. It can be reappropriated to implement the interface outside of the attribute or type class.

class ExternalInterfaceImpl : public Interface::FallbackModel<ExternalInterfaceImpl> {
  void func(Attr/Type pseudoThis, args...) {
    /* implementation here, has access to the concrete type */
    /* through pseudoThis */
    /* the dispatch to this function _already works_ */
  }
};

The only remaining part is registering this interface with the attribute or type. The interface instances are stored in a map within AbstractAttribute/AbstractType. It is straightforward to provide an API of the kind ConcreteAttribute::registerInterface<InterfaceName>(MLIRContext *) that looks up the AbstractAttribute instance in the given context and mutate it given some minimal API with reduced visibility.

An extra step is necessary to support default implementations. In the regular case, they are placed in the trait class that is derived by the attribute or type class. They also have access to the Concrete type and to this (via the $_self special variable in ODS that gets rewritten). A similar approach can be taken here by providing an additional class that takes as template parameters both the interface implementation class and the attribute or type class for which the interface is being implemented.

template <typename Impl, typename Concrete>
class ExternalModel : public FallbackModel<Impl> {
  void funcWithDefaultImpl(Attr/type pseudoThis, args…) {
    /* $_self is replaced with pseudoThis.cast<Concrete>() */
    /* Concrete is readily available here */
  }
}

This can be derived by the implementation instead of FallbackModel and makes the default implementations of the interface methods available.

It is still valuable to have FallbackModel available for use directly as the default implementation may not be suitable for the concrete attribute/type class, and our premises are not to modify it. For example, it may be using $_self.someFunc() where someFunc is not available in the class. In this case, the default implementation can be replaced with the one in FallbackModel and the code calling someFunc, located in the ExternalModel template, is never instantiated so it doesn’t lead to compilation issues.

3 Likes

I can’t comment on all the implementation details you discussed, but again and again I have run into the problem of needing to apply an interface I define to another dialect’s stuff, so +1 in general.

Is there any way to extend this so that it works for OpInterface’s? That’s the situation I run into more frequently than Type/Attribute interfaces.

I haven’t tried, but it seems possible to have this for operations as well with some more code. AbstractOperation also has an interface map that can be modified similarly to AbstractAttribute/Type. Will take a look next week,

Yes, please!

I believe this is crucial to enable more abstraction. I would like to abstract over built-in types by defining interfaces with properties I care about for transformations but I currently can’t.

+1 It looks very useful, esp. for downstream development where people want to augment attributes/types in the upstream.

I just saw that this landed (patch). Thanks Alex!!

Are there some best practices around this? In Rust they have an analogous situation with traits (“trait coherence”) , and have the following rules (enforced by the compiler in this case, though I don’t think we have the mechanics available to be strict), which form a type of “ODR” for them. Effectively, the rule states that to apply a trait to an op, you must either 1) “own the trait”, or 2) “own the op”. This prevents random third-party packages from monkey-patching things, which seems a pretty desirable situation, and is otherwise ripe for abuse.

These look very reasonable, do you mind adding them to the doc? Separate registration is primarily meant to support the (1) case whereas (2) has been the historic approach.

In practice, I would expect external interface models for specific ops to be classes within an anonymous namespace, similarly to pattern or pass definitions. This is good for compile-time and codebase management reasons, and will also prevent third-party packages from directly accessing these.