Dialect conversion
DialectConversion rewrites ops toward a target legality, by predicate or op list, and full conversion differs from partial.
Legality is the declaration
// sketch: MyTensorType stands for a type your dialect defines.
ConversionTarget target(getContext());
target.addLegalDialect<arith::ArithDialect, func::FuncDialect>();
target.addDynamicallyLegalOp<func::FuncOp>([](func::FuncOp f) {
return llvm::none_of(f.getArgumentTypes(),
[](Type t) { return isa<MyTensorType>(t); });
});
The target states which operations are acceptable after conversion; it says nothing about how to get there. Anything not legal must be matched by a pattern, and a pattern that matches an operation the target already considers legal does not fire. markUnknownOpDynamicallyLegal is the escape hatch, and using it broadly is how a conversion stops converting.
Patterns carry a type converter
OpConversionPattern receives a ConversionPatternRewriter and the pattern’s TypeConverter:
struct LowerMake : OpConversionPattern<MyMakeOp> {
using OpConversionPattern::OpConversionPattern;
LogicalResult matchAndRewrite(MyMakeOp op, OpAdaptor adaptor,
ConversionPatternRewriter &rewriter) const override {
Type ty = getTypeConverter()->convertType(op.getType());
if (ty) {
rewriter.replaceOpWithNewOp<memref::AllocOp>(op, ty.cast<MemRefType>());
return success();
}
return rewriter.notifyMatchFailure(op, "no conversion for result type");
}
};
TypeConverter::addConversion registers the type rules, and source and target materialisations tell the framework how to bridge a type it cannot convert directly. The snippet is a sketch of the API, not a compilable pass.
Unrealized casts mark the seam
When an operand’s type changes and no materialisation exists, the driver inserts builtin.unrealized_conversion_cast so the IR stays valid. Survivors are an unfinished conversion. -reconcile-unrealized-casts removes redundant pairs, and mlir-translate rejects a module that still contains one. Confirm the pass name on your pinned release.
Full or partial conversion
applyFullConversion succeeds only if every operation in the converted subtree ends up legal, so an operation nobody wrote a pattern for is an error at the point it appears. applyPartialConversion leaves anything you never marked illegal in place, which is what makes it useful on mixed IR and what allows a missing lowering to pass as success.
A lowering to a self-contained target dialect wants full conversion. A partial lowering that only rewrites a few ops wants partial conversion, scoped to the dialect it touches. Confirm both names against the header on your release.
Things that break
A dynamic legality callback that is too permissive allows an operation to stay, the pass reports success, and the leftovers fail in a later pass or at translation. The other failure is structural: marking an op legal does not exempt its regions, so a lowering can half-convert a body while the outer op looks settled.
Sources
- Dialect Conversiondocs · LLVM 19
- Chapter 5: Partial Lowering to Lower-Level Dialects for Optimizationdocs · LLVM 19
- Builtin Dialect: unrealized conversion castdocs · LLVM 19
A version records the documentation the claim was checked against, not a house pin. Confirm flags with --help on the release you installed.
Check yourself
You mark the `func` dialect legal and expect the function bodies to be left alone. What does the driver do?
`mlir-opt` exits zero but the output still contains `builtin.unrealized_conversion_cast`. What does that mean?
Which conversion mode should a lowering to LLVM use?