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.