Step 06 · Progressive lowering — what a "real" pipeline looks like

Emitting llvm dialect directly skips MLIR's superpower: progressive lowering. Here's the shape of a fuller pipeline you would build once you have a custom dialect.

A hypothetical minilang dialect

module {
  minilang.func @add(%a: !minilang.value, %b: !minilang.value) -> !minilang.value {
    %r = minilang.add %a, %b : !minilang.value
    minilang.return %r : !minilang.value
  }
  minilang.func @main() {
    %a = minilang.const #minilang.num<40> : !minilang.value
    %b = minilang.const #minilang.num<2>  : !minilang.value
    %c = minilang.call @add(%a, %b) : (!minilang.value, !minilang.value) -> !minilang.value
    minilang.print %c : !minilang.value
    minilang.return
  }
}

The !minilang.value type encodes our boxed runtime value (Nil / Bool / Number / Str / Fn).

Lowering passes

mlir-opt minilang.mlir \
    --minilang-specialise-numeric          # unbox numeric ops where types prove safe
    --convert-minilang-to-func             # minilang.func/call → func dialect
    --convert-minilang-to-arith            # numeric ops → arith
    --convert-minilang-to-cf               # control flow lowered
    --convert-minilang-to-memref           # boxed values → struct in memref
    --convert-arith-to-llvm
    --convert-cf-to-llvm
    --convert-memref-to-llvm
    --convert-func-to-llvm
    --reconcile-unrealized-casts
  | mlir-translate --mlir-to-llvmir
  | lli

Each --convert-* is a RewritePattern set authored once and reused across every MiniLang program. That's the value MLIR offers: a ready-made pattern infrastructure plus a dozen battle-tested target dialects.

Domain-specific optimisation

Before lowering to arith, the minilang dialect can run high-level passes: type-specialise polymorphic numeric ops, sink GC barriers, inline closures whose upvalues are constant. Those are nearly impossible to do once everything is i64s and pointers.

What we lose by going straight to llvm

  • No structured loops (scf.for) so loop transformations like --affine-loop-unroll and --scf-parallel-loop-fusion are out.
  • No higher-level type info — every value is i64.
  • No room for domain-specific peepholes.

For a numeric scripting language those losses are small; for ML or DSP workloads they're enormous.