A hands-on, stage-by-stage walkthrough of MLIR for someone who knows the
words (dialect, op, pass, lowering, pattern) but hasn't actually used
any of them. By the end you'll have built a tiny calc dialect, written
canonicalization patterns and a pass, lowered it all the way to LLVM IR,
and JIT-executed it.
The example dialect is deliberately trivial — one type (i32), four ops
(const, add, mul, print) — so you can focus on MLIR mechanics
instead of language semantics.
Each stage lives in its own directory stageNN-…/ and is fully
self-contained:
stageNN-…/
├── README.md ← read this first
├── code/ ← starting boilerplate with `TODO` markers
├── solutions/ ← reference implementation, peek when stuck
└── tests/ ← run these to know you've done it right
Workflow per stage:
- Read the stage's
README.md. - Edit files in
code/to do the tasks. - Run the tests until they pass.
- If stuck, compare against
solutions/.
Stages build on each other conceptually, but each stage's code/ is a
fresh snapshot — you don't need to keep your previous stage's work
around. (Stage N's code/ is roughly equivalent to stage N-1's
solutions/ with new TODOs added.)
| # | Stage | What you'll learn |
|---|---|---|
| 00 | Hello MLIR | Toolchain, .mlir syntax, running mlir-opt |
| 01 | Empty calc dialect |
ODS basics, dialect registration, Bazel tablegen wiring, custom calc-opt driver |
| 02 | calc.const |
First op, attribute vs operand, assembly format, round-trip testing |
| 03 | calc.add + calc.mul |
Binary ops, traits (Pure, Commutative, SameOperandsAndResultType) |
| 04 | calc.print |
Side-effecting ops, MemoryEffects<[MemWrite]>, DCE preservation |
| 05 | Verifiers | calc.shr with hasVerifier, custom verify(), --verify-diagnostics |
| 06 | Canonicalization with DRR | Declarative rewrite patterns: x+0→x, x*1→x, x*0→0 |
| 07 | Fold methods | hasFolder, ConstantLike, materializeConstant; 1+2→3, chained folds |
| 08 | Custom pass | ODS pass declarations, OpRewritePattern, --calc-strength-reduce |
| 09 | Conversion to arith |
ConversionTarget, OpConversionPattern, partial conversion |
| 10 | Conversion to LLVM dialect | LLVM dialect, printf prelude, full lowering pipeline |
| 11 | End-to-end execution | mlir-cpu-runner JIT, stdout-driven FileCheck |
| 12 | Custom type (capstone) | TypeDef, parameterised type, type-level verifier |
This project uses Bazel with the LLVM 17 BCR module
(MODULE.bazel). All MLIR/LLVM targets come from @llvm-project//mlir
and @llvm-project//llvm.
# Build a target
bazel build //stage00-hello-mlir/...
# Run tests for a stage
bazel test //stage00-hello-mlir/...
# Run mlir-opt manually (after building once)
bazel run @llvm-project//mlir:mlir-opt -- --help
# Refresh compile_commands.json for clangd
bazel run //:refresh_compile_commandsThe first build is slow — Bazel will fetch and compile LLVM/MLIR. Plan for 30+ minutes on the first run. Subsequent builds are incremental and fast.
Terms in roughly the order they appear in the tutorial.
- MLIR — Multi-Level Intermediate Representation. A framework for building compiler IRs. Comes from LLVM.
- Operation (op) — The fundamental unit of computation in MLIR.
Examples:
arith.addi,func.call, our futurecalc.add. An op has operands (inputs), results (outputs), attributes (compile-time constants), and may contain regions. - Type — The type of a value (an operand or result). E.g.
i32,f64,tensor<4xi32>, or a custom type you define. - Attribute — Compile-time constant data attached to an op. E.g. the
literal value
42inarith.constant 42 : i32is an attribute, not an operand. - Region — A nested area inside an op that contains blocks. Lets ops
like
func.funcorscf.ifcarry executable bodies. - Block — A sequence of ops ending in a terminator (e.g.
func.return). Blocks live inside regions. - Dialect — A logical grouping of ops, types, and attributes under a
namespace.
arith,func,scf,llvmare all built-in dialects. You're going to build one calledcalc. - ODS — Operation Definition Specification. A DSL (built on
TableGen) for declaring dialects, ops, types, and attributes in
.tdfiles. Generates C++ boilerplate. - TableGen / tblgen — The LLVM code-generation tool that consumes
.tdfiles.mlir-tblgenis its MLIR-aware flavor. - Trait — A reusable behavior tag attached to an op declaration.
Examples:
Pure(op has no side effects, can be removed if unused),Commutative(operand order doesn't matter). - Verifier — A check that an op is well-formed (types match, count of operands is correct, etc.). Some are derived from traits; some you write by hand.
- Pattern (rewrite pattern) — A rule that transforms one piece of IR into another. Used for canonicalization, optimization, and conversion.
- DRR — Declarative Rewrite Rule. A way to express simple rewrite patterns directly in ODS, without C++.
- Canonicalization — A standard MLIR transformation that applies
registered rewrite patterns to simplify IR (e.g.
x + 0 → x). - Pass — A transformation that walks the IR and changes it. Runs as
part of a pass pipeline.
--canonicalizeand--cseare built-in passes. - Lowering / Conversion — The act of translating ops from one
dialect to ops in another (usually lower-level) dialect. E.g.
calc.add→arith.addi→llvm.add. - ConversionTarget — During a dialect conversion, declares which dialects/ops are legal (allowed in the output IR) vs illegal (must be converted away).
- Partial vs full conversion — A partial conversion is OK with some illegal ops surviving; a full conversion fails if any illegal op remains.
mlir-opt— The driver tool for running passes on.mlirfiles.mlir-translate— Converts MLIR IR to/from external formats (LLVM IR, SPIR-V, etc.).mlir-cpu-runner— JIT-executes a.mlirfile end-to-end.- FileCheck — LLVM's pattern-matching test tool. Reads
// CHECK:comments from a test file and verifies they appear in the output of some other command (typicallymlir-opt).