[RFC] DefaultValueInterface for types to provide a default instance

Right now there is no common API to invoke a Value containing an arbitrary instance of a specific type. This is for example desirable when creating dialects working on memory, in which one would like to get initializers for newly created memory locations. The only way to achieve something similar is to define procedures to generate operations creating values for every types one wants to support, creating a lot of inter-dialect dependencies and making it hard to expand upon.

Instead, I propose we create a simple DefaultValueInterface that would provide a method capable of creating (or fetching) a value containing a default value for the types that implement the interface. I would expect the interface to look something like this:

def DefaultValueInterface : TypeInterface<"DefaultValueInterface"> {
  let cppNamespace = "::mlir";
  let description = [{
    This interface provides a common API to construct default values for types
    on demand. This is useful when an instance of a type is needed with no
    requirement on which specific instance.

    The default value implementation of a type can provide guarantees on which
    instance is the default, at the discretion of the implementor.
  }];
  let methods = [
    InterfaceMethod<[{
      Constructs or obtains a default value for this type.

      The builder's insertion point is set where the default value must be
      live.
    }],
    "::mlir::Value", "getDefaultValue", (ins
      "::mlir::OpBuilder &":$builder
    )>,
  ];
}

This interface could live in the builtin dialect, in BuiltinTypeInterfaces.td.

Let me develop on a specific example of why this would be useful. When implementing mem2reg interfaces for MemRef, one needs to specify a default value in case a load would happen before a store or if an allocated value is live in a dead branch. Because MemRef supports arbitrary types using MemRefElementTypeInterface, one cannot always provide a default for the mem2reg interfaces. The only alternative approach is to hardcode a set list of supported default types, creating hard to expand type switches and introducing unnecessary dependencies to MemRef (like for example the complex dialect).

An issue with this approach is that it is not always obvious which instance of a type should be the default. However, I think it can still be useful without specifying any property about the default value beyond it being of the expected type, for example in the example above. The type can implement more specific interfaces for values with more guarantees (for example addition neutral or multiplication neutral elements for rings).

Another issue is that sometimes a default value is worse than a generic undefined value. For example, in the LLVM dialect implementation of the mem2reg interface, llvm.undef is used instead of a default (for example say 0 for integers) because that would allow further optimization on this undefined value (and also because it is the LLVM-specified behavior, but that is coincidental). However, I think having default per-type do not prevent using better values like this. In the context of MemRef, there is no notion of undefined value so defaults are the next best thing, but in dialects where undefined values exist they can simply be used instead. It may also be the case that one in fact wants to specify a consistent and defined default.

If this addition sounds interesting to you, I will open a revision to add it soon after. Any comment is definitely welcome!

@kuhar I think considering your work on poison semantics you may have an interesting opinion on this.

4 Likes

I’d not be quite sure what the default value represents, and to your point default could mean different things in different contexts for different folks (it almost seems like it is a 2D case of default-value(pass/context, type)). E.g., what would the default value of IntegerType be?

I think the point here would be to just pick one. No expectation whatsoever on what it is, it could be a poison value, even. The interest is to have something well defined as the “default”, for anything that wants to take types generically with a default value fallback that is well defined.

About what the default for IntegerType would be, 0 seems nice! But it could as well be 0x8FA, that does not really matter. What matters is that you can consistently get a placeholder for integers, or any other type generically, if one is needed.

Another problem I thought about is implementing this interface for builtin types. For arbitrary types in which types can “depend” on operations that create them, there is no problem. But for builtin types like for example integers where the expected way to create constants is by using arith, this would not be implementable in a reasonable way (you could create an external model in arith but that is all but terrible design in this context).

If there was an integer, float, complex, … dialect of course that would not be an issue. But here, the types that would most likely benefit the most from this interface cannot implement it in a reasonable way…