Sure (also happy to chat f2f if easier). Operations are the most thought out: attributes and types (outside the built-in set) are quite a bit less advanced. John and the circt folks have been pushing the bounds on this the most, which leads us to this discussion.
For Operations
Take this tablegen excerpt from the memref dialect as an example:
@_ods_cext.register_operation(_Dialect)
@_ods_extend_opview_class(_ods_ext_module)
class AssumeAlignmentOp(_ods_ir.OpView):
OPERATION_NAME = "memref.assume_alignment"
_ODS_REGIONS = (0, True)
def __init__(self, memref, alignment, *, loc=None, ip=None):
operands = []
results = []
attributes = {}
operands.append(memref)
attributes["alignment"] = alignment
super().__init__(self.build_generic(
attributes=attributes, results=results, operands=operands,
loc=loc, ip=ip))
@property
def memref(self):
return self.operation.operands[0]
@property
def alignment(self):
return _ods_ir.IntegerAttr(self.operation.attributes["alignment"])
@alignment.setter
def alignment(self, value):
if value is None:
raise ValueError("'None' not allowed as value for mandatory attributes")
self.operation.attributes["alignment"] = value
Here, as on the C++ side, there is a split between generic Operation
and ODS-generated wrappers (which we call OpView
and is similar to the C++ side Op
nomenclature – I just avoided perpetuating Op
/Operation
because it made this API quite a bit more obtuse). When the Python OpView
sublcass is defined, the function register_operation
is called with its class (here done as a decorator but that is just Python for a function call that takes the class it is attached to).
Then on the C++ implementation of register_operation
, we get the attribute OPERATION_NAME
, which ODS generates, and stash it in a map of {OperationName->Python OpView subclass}.
For builder style APIs, this is where it all ends: you build things of the right type and you are done. For traversal, however, when the Python API is mapping a C++ Operation*
to a Python Operation
instance, it checks this registerd operations map and sees if it has a more specific subclass for the operation name. If it does, it returns one of those instead of the generic Operation
instance.
The effect is that, for traversal, you automatically get fully typed operations and have access to their ODS generated attributes, helper methods, etc. When we built the Python API, we built it more for builder-style interactions but did model this so that traversal worked too.
For attributes and types
Attributes and types in Python have none of that kind of sophistication. Each one is a Pybind11 custom class definition (although using some helpers to reduce boiler-plate) which interacts with the backing C-API for the attribute/type. By convention, they all:
- Inherit from either
Attribute
or Type
.
- Have a one-arg
__init__
which takes an existing attribute/type, type checks it and allows the instance to be constructed if it type checks (via the C-API IsA
functions).
- Have
get*
static functions for newing up specific instances.
Again, since our original focus was on builder style APIs, this works just fine. In the rare instance that you have a generic Attribute
or Type
, you just manually “down-cast” it like MyCustomAttribute(some_attr)
.
What I think @jdd is looking for is an auto-downcasting mechanism similar to what we get for operations. That way (I presume), it is more natural to access IR constructs just via “dotted” notation and let the system be responsible for giving you back the right concrete Attribute
/Type
subclass.
Honestly, we’ve done the bare minimum needed for attributes and types on the python side, and I feel that some evolution here is called for. It’d be good if that was convergent in some way with the newer ways of using Tablegen to create them, but I’m not read up enough on that to have an opinion. Fundamentally, the disconnect between them and operations is pretty deep, though: Operation
defines a fully general API for accessing everything about the operation. But the base Attribute
and Type
classes are really just there as an anchor and for identity/interfaces/etc: everything interesting about Attributes/Types comes by way of a C++ (and C, and Python) dedicated API.
Does that help?