Argument and Result Attributes in the C API

MLIR supports argument and result attributes, as discussed here.

In the C bindings, arguments are results are represented by MlirValue which I don’t see a way to attach attributes to. Is this just something that hasn’t been implemented in CAPI or is there something I’m missing?

I don’t seem to find anything about argument and result attributes in that part of the document. Generally, the best thing to look at is the C++ code. Operands and results are (essentially) Values, there is no mechanism to attach attributes to values in MLIR.

FunctionLike ops, on the other hand, do have support for “argument attributes”, but it is specific to the semantics of those ops. They are implemented as operation attributes whose names are derived from positions of function arguments (which themselves are not the operands of the func op—it has no operands—but rather the arguments of the FunctionType stored as TypeAttr that specifies the type of the function defined by the op). These attributes are of DictionaryAttr type and the semantics of the FunctionLike ops say these attributes are associated with function arguments. These can be accessed like any other attributes, we just have utility functions in C++ to do that faster.

It is in the example from that section:

// A function with an argument attribute
func @example_fn_arg(%x: i32 {swift.self = unit})

// A function with a result attribute
func @example_fn_result() -> (f64 {dialectName.attrName = 0 : i64})

// A function with an attribute
func @example_fn_attr() attributes {dialectName.attrName = false}

I thought it might be convention-based, but I didn’t find any documentation to that effect.

It’s not much, but there is also a little blurb about the FunctionLike trait in the traits documentation that explains a little bit about the argument and result attributes.

I can’t find where it was discussed previously right now, but we have discussed possibly having C-API support for FunctionLike ops, vs open coding this at the leaves. There are some intricate things there that I think would be better if they had an API around. But I haven’t taken the time to form more of a thought than that.

Great! I may take a first pass at what something like this might look like.

Might also have a quick look through recent work/discussions on this. I have it in my mind that someone was working on generalizing some of the FuncOp stuff, and I was waiting to see where that went before thinking about a C-API. Sorry I don’t have more details than that – on mobile.

It might have been a review thread or a chat. This was in the context of autogenerating Python custom-op APIs and I was arguing that FuncOp is a special thing in the built-in dialect so it wasn’t representative of other ops.

It’s not even a convention, it’s just the semantics of a very small set of ops. Try feeding that example into mlir-opt -mlir-print-op-generic to see how it looks like without all the syntax sugar.

Fwiw - here is what I consider an anti-pattern for bit-banging the C API to create a function. It is similarly hard for other common things you want to do to them and feels like it creates a good deal of unneeded coupling to implementation details for such a common thing: torch-mlir/func_builder.cpp at 67d6694fdc427e81f18acf4ce4e61218c83dacc1 · llvm/torch-mlir · GitHub

@ftynse has this right: argument/result attributes are a property of the FunctionLike op and kept on attributes on that op. Adding/removing them should be helpers for the func op itself, not on MlirValue.

The discussion of adding attributes to a Value is a long one but unrelated to function arg/result attributes. See e.g. Using a dialect to represent analysis information

Actually it is just as slow :slight_smile: It doesn’t do anything special under the hood except literally construct a string "result" + Twine(i) and do a normal attribute lookup.

The question is where do we stop? FuncOp is “special” because it’s a historically built-in thing, so we can have a custom class for it in the C API. ModuleOp is special because of the parser and ownership model, also fine. Should we also have custom classes for LLVMFuncOp and GPU modules? For linalg::GenericOp with its dozen of op-specific attributes? All of those will otherwise need some messing with the generic form of the op.

By “faster” I meant faster to write, not faster to execute.

Thanks for the input, everyone.

For Swift bindings, I don’t think it makes sense to wrap FuncOp but it may make sense to present some of these semantics in the API. For instance, a method like mlirNamedArgumentForResult might be interesting, but these are small improvements. This could change when we start using the TableGen to generate Swift code.

I’d recommend we stop at Module. I don’t think that Module is being treated specific because of historic significance, I think it is treated specially because of the memory ownership behavior. This doesn’t apply to other levels of ops.

I believe that module by itself does not have more ownership than any other op (an example is that you can nest module within another module).
Any op can be expressing the ownership, and we have a template for this here:
For example the SPIRV deserializer produces a top-level spv.module and not a regular module.

The only places where ModuleOp is hardcoded as far as I know are the textual parser and the PassManager (which at the moment requires to be rooted on a module). These aren’t that is that deeply engrained by the way, we could change that fairly easy I believe.

It is historical in the sense that the parser was one of the first things in MLIR at a time where the infra wasn’t extensible, and we preserved this because it makes test cases easier to write.
But an easy alternative could be to use injection: the parser could take a Block as argument and populate it. Then mlir-opt would default to create a ModuleOp and pass the body to the parser.

Could you please rephrase this? I couldn’t tell if there is a typo. Canceling out the negatives, did you mean “I believe that the module has more ownership than any other op”? Ownership of what? Memory? And what does it mean to have more or less of ownership of memory?!

Maybe like this:

I was providing a counter-point to:

Ok, I was referring to OwningModuleRef which is still hard coded in a couple places (like mlir-translate hooks etc) but I agree with you that this isn’t core. Generalizing this so the C API can work analogously to OwningOpRefBase<T> makes sense to me.