Making the ODS-generated dialects the default in Melior
- Date: 2026-11-08
- Model: Claude Opus 5.5
- Repository:
mlir-rs/melior - Version:
bug/unnamed-operandat917be0139, one commit on top ofmainate6407a664
Summary
- The goal is to retire the hand-written modules
melior::dialect::{arith, cf, func, index, llvm, memref, scf}and make the generatedmelior::dialect::ods::*the default API. - Coverage is not the problem. All seven hand-written dialects are among the bundled generated ones, and the
ods-dialectsfeature adds about 7 seconds tocargo checkand 11 seconds tocargo buildof the crate. - Four blockers remain: result type inference (179 builders demand result types that MLIR can infer), typed attributes (half of all attribute parameters are an untyped
Attribute), the non-op items in thellvmmodule, and a migration path for functions that share names but not signatures. - The main ergonomic gaps are that typed operations are lost once appended to a block, optional fields cannot be set conditionally, variadic-of-variadic operands are flattened, properties are dropped, and fields named
namebecome_name. - Five generator defects are still open, and none of them are needed to switch but all of them affect a default API.
Method
- Generated API: the bundled dialects were expanded with
cargo expand --all-features dialect::ods, and the signatures of every generated constructor and builder setter were extracted (1,257 operations). - Result inference: a runtime probe asked MLIR, through
mlirOperationImplementsInterfaceStaticwithmlirInferTypeOpInterfaceTypeID, whether each operation whose builder demands result types implementsInferTypeOpInterface. All 804 such operations were registered. - Attributes and arguments: the bundled ODS files were dumped with
llvm-tblgen -dump-json, and the attribute parameters, properties, and variadic-of-variadic operands of 1,191 operations were classified (a few bundled files were not dumped). - Build cost:
cargo check -p meliorandcargo build -p meliorwere timed with and withoutods-dialects, with dependencies already built andCARGO_INCREMENTAL=0. - Code reading: all seven hand-written modules, the
helperstraits, the README, and the generator inmacro/src/dialect.
Blockers
B-1. 179 builders demand result types that MLIR can infer
- 804 of the 1,257 generated builders require result types, and only 36 use interface-based inference. The runtime probe found that 179 of those 804 operations implement
InferTypeOpInterface. - These include exactly the operations whose hand-written helpers infer types:
arith.cmpi,arith.cmpf,arith.constant, andarith.select;memref.load,memref.rank, andmemref.dim; all 24indexoperations; andscf.if. For example, the generatedarith::constant(context, result, value, location)needs a type the hand-writtenarith::constant(context, value, location)infers, andindex::add(context, result, lhs, rhs, location)asks for a result that is alwaysindex. - By dialect, the 179 operations are
index24,amdgpu24,gpu19,llvm17,shape13,sparse_tensor10,x869,async9,vector8,complex7,memref6,irdl6,arm_sme6,arith6,tensor5,math5,affine3,scf1, andlinalg1. - Cause: the macro uses inference only when the TableGen trait list names
InferTypeOpInterface, and never for operations with regions (lines 86 to 87 ofmacro/src/dialect/operation.rs). MLIR’s own generator also adds the interface whenever every result type is derivable, from buildable types such asIndexandI1or from relations such asAllTypesMatchandTypesMatchWith. - Fix options: mirror that rule in the macro, or make result setters optional and call
enable_result_type_inference()inbuild()when none were set. Either way, lift the region exclusion.
B-2. Half of all attribute parameters are an untyped Attribute
-
607 of 1,218 attribute parameters fall through the storage type mapping in
build_storage_type(lines 68 to 82 ofmacro/src/dialect/operation/attribute.rs) and become an untypedAttribute. The largest groups are below, and dialect enum attributes make up much of the rest.Storage type Parameters ::mlir::UnitAttr149 ::mlir::DenseIntElementsAttr97 ::mlir::arith::FastMathFlagsAttr84 ::mlir::LLVM::FastmathFlagsAttr55 ::mlir::AffineMapAttr21 ::mlir::SymbolRefAttr8 -
Parity is lost here: the hand-written modules provide
arith::CmpiPredicate,arith::CmpfPredicate, andllvm::attributes::Linkage, while the generatedarith::cmpi(..., predicate: Attribute, ...)andindex::cmp(..., pred: Attribute, ...)give callers no safe way to build those attributes. -
Melior has no wrappers for unit, integer-element, affine-map, or symbol-reference attributes, or for any dialect enum attribute.
-
Fix: add core wrappers for those attributes (a unit attribute is better as a
boolparameter) and generate Rust enums from TableGen’s enum attribute records.
B-3. The non-op parts of llvm have nowhere to go
- The
llvmmodule also holds the type constructorspointer,struct,array,function, andvoid(melior/src/dialect/llvm/type.rs), theLinkageattribute builder (attributes.rs), and the option buildersAllocaOptionsandLoadStoreOptionsbehindllvm::alloca,llvm::load, andllvm::store. - It also has semantic helpers that a generated builder does not offer, such as
get_element_ptr_dynamic, which writes the dynamic-index markeri32::MINintorawConstantIndices. - The macro reads only
Oprecords and generates no types or attributes, so retiring the module needs either a permanent hand-written home for these items or type and attribute generation from TableGen’s type and attribute definitions.
B-4. Same names with different signatures leave no migration path
- The hand-written
arith::addi(lhs, rhs, location) -> Operationand the generatedods::arith::addi(context, lhs, rhs, location) -> AddIOperationshare every function name, so the two cannot live in one module. - Generated constructors also take a
Context, order parameters differently (results first, then operands, regions, successors, and attributes), and sometimes use different names, for exampleextract_valueagainstextractvalueandundefagainstmlir_undefinllvm. - Users of the hand-written functions include the README example, about 49 internal call sites outside the dialect modules, and the
helperstraits, which already mix both sides (ArithBlockExtuses the hand-writtenarithtogether withods::arith::constant, andLlvmBlockExtusesods::llvmoperations). - A deprecation plan is needed, for example
#[deprecated]hand-written functions pointing at their generated replacements for one release beforeods-dialectsbecomes a default feature.
High-impact ergonomics
E-1. A typed operation is lost once it is in a block
- Generated operation structs convert only from an owned
Operation(TryFrom<Operation>around line 89 ofmacro/src/dialect/generation.rs).BlockLike::append_operationtakesOperation(line 90 ofmelior/src/ir/block/block_like.rs), so every call needs.into(), and the typed accessors are gone afterwards because there is no typed view overOperationRef. - The typed structs do not implement
OperationLikeeither. They expose onlyas_operation(). - Recommended: let
append_operationand theinsert_*methods acceptimpl Into<Operation>, implementOperationLikefor the typed structs, and add typed reference wrappers withTryFrom<OperationRef>.
E-2. Optional fields cannot be set conditionally
- Optional operand and result setters change the builder type. For example,
.arg(value)turnsllvm::ReturnOperationBuilder<'c, Unset>intoReturnOperationBuilder<'c, Set>, so the two branches of anif let Some(value)around the setter have different types and callers must callbuild()in each branch. - The hand-written functions take
Optiondirectly:llvm::r#return(Option<Value>),memref::realloc(size: Option<Value>), andmemref::alloc(alignment: Option<IntegerAttribute>). - Setters that take
Option<T>would remove the friction.
E-3. Variadic-of-variadic operands are flattened
- Seven operations have them:
cf.switch,llvm.call,llvm.call_intrinsic,llvm.invoke,llvm.switch,llvm.indirectbr, andllvm.intr.assume. Support is still aTODOat line 27 ofmacro/src/dialect/type.rs. - The generated
cf::switchtakes one flatcase_operandsslice plus a hand-computedcase_operand_segmentsattribute, while the hand-writtencf::switchtakes&[&[Value]]and computes the segments itself.
E-4. Properties are silently dropped
- Operand and attribute collection keep only type constraints and attributes (lines 394 and 458 of
macro/src/dialect/operation.rs), so property arguments disappear. - Seven operations are affected: the overflow flags of
llvm.add,llvm.sub,llvm.mul,llvm.shl, andllvm.trunc, thenoWrapFlagsofllvm.getelementptr, and thedisjointflag ofaffine.linearize_index. - MLIR accepts inherent attributes when it creates an operation and converts them into properties, so builders could set them as attributes.
E-5. Fields named name become _name
- 26 accessors and parameters are called
_name, for examplememref::get_global(context, result, _name, location), becausenameis inRESERVED_NAMES(line 8 ofmacro/src/dialect/utility.rs) to avoid the generatedname()function. - A leading underscore means unused in Rust. Renaming the generated function, for example to
operation_name(), would free the name.
E-6. No way to add discardable attributes
- The hand-written
func::funcandllvm::funcaccept extra attributes such asllvm.emit_c_interface. - Typed builders have no passthrough for them, and typed operations do not expose
OperationMutLike.
Open generator defects
All of these were checked against the code at the version above.
- D-1. For
SameOperandsAndResultType, the setter of a variadic first operand indexes[0]to read the result type and panics on an empty slice (line 226 ofmacro/src/dialect/generation/operation_builder.rs). Since unnamed operands are kept, an unnamed variadic first operand reaches this path too. - D-2. An optional attribute under
FirstAttrDerivedResultTypeadds results on every setter call (lines 244 to 264 of the same file). Setting it twice gives two sets of results, and never setting it gives none. - D-3. The
result_typelocal in those setters uses the caller’s naming context (lines 234 and 259), so a field namedresult_typebreaks compilation with E0308. - D-4. Under
SameVariadicOperandSizeandSameVariadicResultSize, getters of optional elements return an iterator while declaring aResult(line 56 ofmacro/src/dialect/generation/element_accessor.rs), so they fail to compile with E0308. No bundled operation hits this. - D-5.
build()callsexpectwhen MLIR cannot create the operation, for example when type inference fails (line 298 ofoperation_builder.rs). The hand-written helpers behave the same, but a default API would be better off fallible.
Cost and packaging
-
C-1. Build time is not a blocker. The builders are generic, so code is generated only for those a crate uses.
Command Without ods-dialectsWith ods-dialectscargo check -p melior1 s 8 s cargo build -p melior0-1 s 11 s The
cargo buildnumbers are the same for the dev and release profiles. Per-dialect features would be an optimization, not a requirement. -
C-2. Tests need porting. The hand-written modules have 75 tests and 85 snapshot files, while
melior/src/dialect/ods.rshas 16 tests. -
C-3. The
helpersfeature requiresods-dialectsbut mixes both APIs, so it needs rebasing onto the generated one.
Already at parity
- Successor safety: generated successor constructors are
unsafe(76 generated functions), with the same safety contract as the hand-writtencf::br,cf::cond_br, andcf::switch. - Segment sizes: builders write the operand and result segment size arrays since #922.
- Unnamed fields: unnamed results are kept since #974 and #1018, and unnamed operands with
bug/unnamed-operand. - Completeness: several generated constructors cover more than the hand-written ones. For example,
scf::r#fortakes initial arguments and results, which the hand-written one lacks.
Suggested order
- B-1 (inference), B-2 together with E-5 (typed attributes and naming), and D-1 to D-3. These are generator work with no API break.
- E-1, E-2, E-3, and E-6 (ergonomics), with E-4 (properties) folded into the attribute work.
- B-3: decide whether the non-op
llvmitems stay hand-written or get generated. - B-4, C-2, and C-3: deprecate the hand-written functions, port the tests, the README, and
helpers, and then makeods-dialectsa default feature.