Allowing Types to have interfaces and/or traits (like ops do)

Continuing the discussion from [RFC] Tensors with unknown element types:

MLIR’s type system is currently quite a bit more limited than its op system in that many of the built-in (“standard”) types provide functionality that cannot be duplicated by dialect types. I’ve run into this issue multiple times with the ShapedType hierarchy (tensors, vectors, memrefs), and Chris above mentioned similar issues with the integer types.

The issue rarely comes up as a request for a more generic/open type system, but in my experience, the lack of such a thing is what causes us to have protracted discussions on the specific constraints that various standard types enforce (i.e. should tensor allow unknown element types, how do we represent bounded ranges of dims, etc). In the abstract, there is little reason that such things should need to be resolved to a single, global answer. I think the fact that it does today is a by-product of there being only one ShapedType hierarchy and it is impossible to extend it with dialect types without them being completely disjoint and existing on an island all their own.

When I look at ShapedType, isInteger(width), isSignlessInteger(), etc, I see what may be more appropriate to think of as type traits (or type interfaces) with an open system of concrete types that can conform to the trait. For ShapedType it is probably a number of traits, considering the complexity and diversity of real types in the wild that represent such things (i.e. RankedShapedType, StaticDimsShapedType, etc). Even if we grandfathered the ShapedType methods into a ShapedTypeTrait, we’d be better off than we are today, because arbitrary dialects could define their own, possibly adding other domain-specific traits (such as ShapedLayoutTrait, etc). It would be really nice if ODS predicates existed that predicated on a set of such type traits instead of a specific concrete type (for ops that are generic in this way).

I’d like to bring the topic up and get feedback on:

a. Does anyone agree/disagree with the concept in general or have further state on this?
b. If implementing such a thing, do people think it is necessary to model it how operations do (with an optional AbstractOperation that can resolve its traits/interfaces) or could we just build such a thing into TypeStorage (at the cost of an additional pointer)?
c. Thoughts on naming? “Type trait” is more aligned with C++ terminology (vs “Type interface”, “Metatype”, etc).

This would be awesome to have!

I believe this was one of the things that the attribute on Tensor could have helped with too.

I agree at certain levels. E.g., see what we did for TFLite dialect. There we differentiated between what the ops support and what the runtime supports. So you could have foo operating on “integral types” but at the point where you lower to the runtime it gets verified. So what we did there allows for specifying the dialect/ops in a manner that could verify that it could execute while allowing more generic usage for different TFLite backends/consumers (decoupling op set constraints from constraints of backend). It ends up that one writes one constraint, but 2 sets of constraints are created. But still requires that one knows the general type characteristics required (e.g., “forms a Abelian group” is not possible :slight_smile: )

For integer and floats too. The place where I wanted this (and could have sworn I filed a bug but can’t find it), was as I wanted to do the following: compare if an attribute (being some representation of a ℝ) is less than another attribute. So I’d want to be able to say “is this numerical type comparable? is this value less than float x? produce an attribute of this dialect type with value 0 in the numerical system of that type”

This could also be achieved via helper methods. In fact we do this in a few places, but yes those functions are defined in terms of concrete types that can’t be expanded (but that is not a ODS limit, it just means for those functions no “register” method have been added).

Agree conceptually and utility, not sure what is best way to spell it. It adds complexity and not sure of the trade-offs.

So with the AbstractOperation equivalent it would be (effectively) like having a query function/registry separate from the Type for each interface/trait? E.g., have isNumericalType and then have a registerNumericalType so that one could register dialect specific numerical types?

Wouldn’t you have both a Type trait and a Type interface, matching Op trait and Op interface?

Having such traits seems like it could help defining your own dialect types by reusing the core interface (ShapedType as you mention), but this alone has a limited interest if it can’t be generically manipulated.

The reason traits and interfaces are useful on operation is because of the contract with the client, which you embrace by mentioning doing something like AbstractOperation resolving traits/interfaces on TypeStorage.

What I’d be interested in is to see how we’d use these traits in practice? Would we define ops in ODS as operating on type traits/interfaces instead of concrete type?
For example would std.addi accept any value where the type with “ShapedTypeTrait” where “getElementType” returns a type that has the “IntegerLikeTrait”?
(reminds me of C++ concepts…)

That’s interesting but are there other areas that would benefit from this? DRR? Would it simplify type conversion (I don’t see how right now)?

That would tend to be my preference where appropriate. And fwiw - I’ve back-burnered this until River gets back and up to speed, since I know that the current modeling of types is pretty load bearing to things he was considering important and I would definitely want his mind on any extensions.

Hi @River707, this was the thread we were talking about today when you asked if you missed something from me while out.

Late to the party, but my thoughts are generally that traits on attributes/types aren’t really that useful given that there is no generic mechanism with which to introspect. Operations have a defined API on how the internals are represented, and those internals can be manipulated opaquely by anything. This makes traits for operations extremely useful and powerful. On the other hand, I can see how interfaces could be extremely useful for attributes/types. In several known cases, certain parts of the infra would benefit from this. One area is for attributes that store data. For things like IntegerAttr, we only really care about the bitwidth of the integer and don’t really care that the type is literally IntegerType. Ideally, this would help reduce the amount of duplication(or impedance mismatches) that will in occur when dialects with custom types want to store data for those types in attributes. This would help simplify things like constant folding, which want to directly manipulate the values held by attributes.

For those interested in what it would take to achieve this, I’ve uploaded a chain of revisions(currently ending in D81884) that provides the necessary support in the core infra for interfaces/traits:
– River

Have you thought much about expanding IntegerAttr to allowing dialect specific types? The current situation works, but is a bit weird, where you end up with something like:

  mything.constant 42 : !mything.size_t

and the attribute has some IntegerAttr type which mismatches from mything.constant. My understanding of this is that we need an abstract interface to get the bitwidth of the integer type.

Yes, this is one of the major reasons why I’m pro having attribute/type interfaces. I’d really like to remove the llvm.constant(true : i1) : !llvm.i1 weirdness that is currently pervasive.

Right - one minor step along the way would be to keep the current in-memory IR representation, but build out the asm parser/printer logic enough to hooks to enable llvm.constant true : !llvm.i1 to get parsed into that form, perhaps using “transformer functions” of some sort to do type mappings.

That sounds like a good step to switching over the LLVM dialect, though that can also be done after the interface work.