01 — Why an MLIR-Style Framework?
LLVM IR is one intermediate representation. It works beautifully for languages whose operations map onto C-like primitives — integer arithmetic, memory load/store, function call. It works poorly for anything else:
- Tensor compilers want
matmul,convolution,reduceas first-class ops. Expressing these in LLVM IR loses too much structure to recover later. - Hardware DSLs want device-specific ops (
gpu.launch,spirv.kernel,nvvm.barrier0) that LLVM IR has no good way to represent. - Polyhedral compilers want loop nests, affine maps, and dependency information that LLVM scalar evolution can only partially reconstruct.
The historical answer was: each project invented its own IR (XLA HLO, TensorFlow Graph, Halide, Tiramisu, …) and re-implemented passes, printers, parsers, and verifiers. Every project paid the same tax.
MLIR's answer: make the IR itself extensible. Provide a single skeleton (Operations, Regions, Blocks, Values, Types, Attributes) and let each project plug in custom dialects that define their own ops, types, and conversions. Re-use the printer, the parser, the pass manager, the canonicaliser — the whole infrastructure — across every dialect.
What a dialect is
A dialect is a namespace of ops with custom semantics. Examples in real MLIR:
arith.*— integer and float arithmetic.linalg.*— structured linear-algebra ops (matmul, conv).tosa.*— neural-network ops at a higher level.scf.*— structured control flow (if, for, while).memref.*— buffers with strides and offsets.gpu.*,nvvm.*,spirv.*— device dialects.llvm.*— a one-to-one mirror of LLVM IR, used as a lowering sink.
A typical compile looks like a sequence of dialect rewrites:
tosa → linalg → scf + memref → llvm → LLVM IR → machine code
Each step is a pass. Each pass is built from rewrite patterns. Each pattern matches a small subgraph of ops and replaces it with another small subgraph. Eventually no ops from the source dialect remain, and you've lowered the program one tier closer to hardware.
What cp-18 reproduces
We model exactly this skeleton at teaching scale: two dialects
(tiny.* and ll.*), a constant-folding pass, a dead-code-elimination pass,
and a lowering that rewrites tiny.* → ll.*. After it runs, no tiny.
ops remain. That's the MLIR programming model in miniature.
What cp-18 does not reproduce
- TableGen. Real MLIR generates op classes, builders, verifiers,
parsers, and printers from
.tdfiles. We hand-write them. - Pattern matching. Real MLIR has a declarative DSL for rewrite patterns
(
RewritePattern,OpRewritePattern<T>). Our "patterns" are if-chains inlowering.cpp— same logic, no sugar. - Verification. Real MLIR runs structural verifiers on every op. We trust the builder.
- Type system depth. Real MLIR types form a class hierarchy and can carry shapes, layouts, and dialects. Ours are strings.
The framework you build here is not a replacement; it's a reading companion. After it, every concept in MLIR has a hook in your head to hang on.