Possible to use EnumAttr as a type?

Is it possible to use EnumAttr as a type within assemblyFormat?

I have the following enum which im creating tablegens from:

def A: StrEnumAttrCase<"A">;
def B: StrEnumAttrCase<"B">;

def ExampleTypeEnum: StrEnumAttr<"ExampleType", "An example str enum",
                           [A, B]> {
}

And then i want to use it here:

def Example_CreateOp : Example_Op<"example", [NoSideEffect]> {
    let summary = ...
    let description =...    
    let arguments = (ins ExampleTypeEnum:$attr, AnyInteger:$id);
    let results = (outs Example:$example);
    let assemblyFormat = [{
      attr-dict `(` $attr  `,` $id `)` `:` `(` ref($attr) `,` type($id) `)` `->` type($out)
    }];
}

but im getting the following error:

error: expected directive, literal, variable, or optional group
      attr-dict `(` $attr  `,` $id `)` `:` `(` ref($attr) `,` type($id) `)` `->` type($out)

any ideas?

ref is intended to be used when calling a custom directive, it should not be used immediately in the assembly format.

I used ref as an example. i have also used type but it gives an error. i was looking to see how i could change to take an enum directly instead of a StrAttr.

The best way to get help is to give us a minimal reproducible example that results in exactly the error you are trying to resolve. Otherwise we will be looking at wrong errors because of incorrect simplifications or assumptions.

You cannot use type() with attributes, only with operands or results.

Maybe if you explain what you are trying to achieve by “using enumattr as a type”, we will find a better way.

Ok. Ill try to explain:

Atm, I have these pieces of code:

def A: StrEnumAttrCase<"A">;
def B: StrEnumAttrCase<"B">;

def ExampleTypeEnum: StrEnumAttr<"ExampleType", "An example str enum",
                           [A, B]> {
}
def Example_CreateOp : Example_Op<"example", [NoSideEffect]> {
    let summary = ...
    let description =...    
    let arguments = (ins StrAttr:$type, AnyInteger:$id);
    let results = (outs Example:$example);
    let assemblyFormat = [{
      attr-dict `(` $type  `,` $id `)` `:` `(` type($type) `,` type($id) `)` `->` type($out)
    }];
}

In the example above, i would like to use an StrEnumAttr instead of StrAttr to create my op as such:

Builder.create<>(loc, id, ExampleType::A)

But for now i have to do this

Builder.create<>(loc, id, stringify(ExampleType::A))

Is there a way?

For the assembly format, I would think you could do: attr-dict($type ,$id) : (type($id)) → type($out) ; you don’t need to separate the type from the attribute here.

The builder is a separate question: the declarative assembly is unrelated.
You should look at the generated code to see the list of builders that are present, there should be a variant that operates on the enum.

Thanks. I understood and this worked. however the builder is still only accepting StringRef or StringAttr. any ideas how i could use the enum?

I checked the codebase, we don’t generate builders for these because they are stored as StringAttr.
The generated builders would hide some non-trivial cost (stringify + context lookup for the StringAttr).

You can easily add these to your op though

let builders = [
    OpBuilder<(ins "ExampleEnum":$type, "Value":$id),
    [{
      auto typeAttr = StringAttr::get($_builder.getContext(), stringifyEnum($type));
      build($_builder, $_state, typeAttr, id);
    }]>];

(I haven’t tried to build it, you may have to adjust)

Thanks a lot for this!

I had to change to OpBuilderDAG. is this OK?
Also, it looks like that the below would compile but this results in an infinite recusrion.

    let builders = [
        OpBuilderDAG<(ins "ExampleEnum":$type, "Value":$id),
                  [{
                    build($_builder, $_state, type, id);
                  }]>];

Yes, you’re just calling the same builder… See my code sample: you need to create the StringAttr in your body.

Thanks. Somehow, i have to provide this new argument (ex: ::mlir::Type out) now:

  static void build(::mlir::OpBuilder &odsBuilder, ::mlir::OperationState &odsState, ExampleEnum type, Value id);
  static void build(::mlir::OpBuilder &odsBuilder, ::mlir::OperationState &odsState, ::mlir::Type out, ::mlir::StringAttr type, ::mlir::IntegerAttr id);
  static void build(::mlir::OpBuilder &odsBuilder, ::mlir::OperationState &odsState, ::mlir::TypeRange resultTypes, ::mlir::StringAttr type, ::mlir::IntegerAttr id);
  static void build(::mlir::OpBuilder &odsBuilder, ::mlir::OperationState &odsState, ::mlir::Type out, ::llvm::StringRef type, ::llvm::APInt id);
  static void build(::mlir::OpBuilder &odsBuilder, ::mlir::OperationState &odsState, ::mlir::TypeRange resultTypes, ::llvm::StringRef type, ::llvm::APInt id);
  static void build(::mlir::OpBuilder &, ::mlir::OperationState &odsState, ::mlir::TypeRange resultTypes, ::mlir::ValueRange operands, ::llvm::ArrayRef<::mlir::NamedAttribute> attributes = {});

any idea what i need there?

The generated builders require a type for the result of the operation because it can’t be inferred from the arguments and it isn’t a “buildable” type (a type that does not need any argument to be built)

Sorry, but i dont follow. is it expected that this would result in:

Not this

auto result = Builder.create<>(loc, id, ExampleType::A)

and Instead this

// i guess this is not even passed by reference...
 Builder.create<>(loc, result, id, ExampleType::A) 

What the role of let results =... in this case?

You have in your ODS above let results = (outs Example:$example); ; that says that:

  1. your operation will have one result only: the verifier will fail if you create such an operation with no result or more than one. A OneResult trait will be added to the operation, which make available the C++ method Value getResult();.
  2. The type of the result must be Example. You’re not providing the definition for Example here so I can’t say much more about it. If it was a AnyTensor we’d would check that the Type matches the predicate .isa<::mlir::TensorType>() for example, so that if you were to use something else for result it would fail the verifier.
  3. The name of the result is example. There will be an accessor generated for it Value example();.

When you create the operation, you need to provide the type for this example result. This type must be an instance of Example. Again without knowing moe about Example, I can’t be specific. But if it was AnyTensor then you need to provide an instance of a TensorType when creating your operation.

Now if the Example type is something like a very specific type, let’s say an i32. It seems unnecessary to provide it explicitly as it is unambiguous. Such types are defined in ODS as a “BuildableType”. The generated builder would omit them from the list.

Finally, there are operations which can infer the result type from the arguments provided. For example if the operation declares the trait SameOperandsAndResultType then it will generate a builder that does not take the result type as argument and it’ll use the argument type instead.

1 Like

Thanks a lot for the explanations. this helps a lot understanding how the underlying system works.

FYI,

StrEnumAttr has genSpecializedAttr property (which is off by default). If it is set to 1, the mlir-tblgen will generate extra Attribute class, which will wrap conversion from string and the enum class. Something like that:

enum class MyEnum { ... };

class MyEnumAttr : public StringAttr {
    using ValueType = MyEnum;
    static MyEnumAttr get(MLIRContext* ctx, MyEnum val);
    MyEnum getValue() const;
    static bool classof(Attribute attr);
};

This will also switch all StrEnumAttr users to the MyEnumAttr and MyEnum directly. MyEnumAttr will perform string <-> enum conversions internally. This feature is disabled by default, since it has performance penalty (the string <-> enum conversion).

The similar Attribute class is also generated for IntEnumAttr and it is enabled by default, since for that case there is no performance penalty (int <-> enum conversion is cheap).