Why does `#ub.poison` not carry a type?

I noticed an asymmetry in the design of ub.poison and #ub.poison.

// Poison value. Folds to #ub.poison.
%0 = ub.poison() : i32

Notice that you can fold ub.poison() => #ub.poison, but you cannot go the other way: you cannot materialize a constant (Dialect::materializeConstant) from a #ub.poison attribute (even though the attribute is ConstantLike). That’s because PoisonAttr doesn’t carry a type parameter.

Edit: You can use materializeConstant because it takes an mlir::Type parameter. But you could say the same about IntegerAttr, so my question still stands.

I expected the following implementation:

def PoisonAttr : UB_Attr<"Poison", "poison", [PoisonAttrInterface, TypedAttrInterface]> {
  let parameters = (ins "Type":$type);
  let assemblyFormat = "`<` $type `>`";
}

ub.poison() : i32 would fold to #ub.poison<i32>. Any thoughts / background that I’m missing? (I have no concrete use case for this in mind. It just stuck out to me.)

I don’t follow? IntegerAttr holds an actual integer value, which is why it has a type to interpret the value. There is not value in a poison attribute.

The rules about folding aren’t clear to me: I would want all constant operation to fold to a Typed Attributes with the same type as the SSA value they represent, but also have a single constant attribute for a given SSA type. That is the folding for i32 should only generate IntegerAttr.
That would exclude your folding of ub.poison I believe.

That would avoid the need for special casing in many places, but clashes with the current ub.poison folder. What was the reason for adding #ub.poison? What’s its purpose?

IIUC, technically, poison has no type. For instance, in LLVM IR you can store an i32 poison into a pointer and then load i16 and i64 from that pointer, and you still get poison. The type comes from the user of the poison value.

So, in theory, if you really need to materialize #ub.poison, you’d materialize to the type of each operation that is using it, at the point of use, in which case, you just pass the mlir::Type as a parameter and it should be fine.

The original rationale for us using untyped poison attr is here: [RFC] Poison semantics for MLIR - #41 by Hardcode84 (cc @Hardcode84).

I think it’s an implementation detail, not a property of poison semantics. If you were implementing an ub-tracking interpreter, you’d probably want to have an extra bit to actually denote if a value of the given type is poison or not, but it’d be up to you to decide if you want to store it on the side or actually extend integer/float types with an extra bit. See how this is denoted in the poison paper: https://web.ist.utl.pt/nuno.lopes/pubs/undef-pldi17.pdf