TL;DR:
Inspired by a conversation with a “fatherly figure” I made something fun:
I present mlir-opt-repl: a pip-installable Python package that provides a repl experience on top of mlir-opt. It’s small but already has lots of “powers”, like reading IR and running passes:
mlir-opt-repl> load -
func.func @matmul(%A: memref<4x8xf32>, %B: memref<8x4xf32>, %C: memref<4x4xf32>) {
linalg.matmul ins(%A, %B : memref<4x8xf32>, memref<8x4xf32>)
outs(%C : memref<4x4xf32>)
return
}
mlir-opt-repl> run canonicalize
module {
func.func @matmul(%arg0: memref<4x8xf32>, %arg1: memref<8x4xf32>, %arg2: memref<4x4xf32>) {
linalg.matmul ins(%arg0, %arg1 : memref<4x8xf32>, memref<8x4xf32>) outs(%arg2 : memref<4x4xf32>)
return
}
}
mlir-opt-repl> run convert-linalg-to-loops
module {
func.func @matmul(%arg0: memref<4x8xf32>, %arg1: memref<8x4xf32>, %arg2: memref<4x4xf32>) {
%c0 = arith.constant 0 : index
%c4 = arith.constant 4 : index
%c1 = arith.constant 1 : index
%c8 = arith.constant 8 : index
scf.for %arg3 = %c0 to %c4 step %c1 {
scf.for %arg4 = %c0 to %c4 step %c1 {
scf.for %arg5 = %c0 to %c8 step %c1 {
%0 = memref.load %arg0[%arg3, %arg5] : memref<4x8xf32>
%1 = memref.load %arg1[%arg5, %arg4] : memref<8x4xf32>
%2 = memref.load %arg2[%arg3, %arg4] : memref<4x4xf32>
%3 = arith.mulf %0, %1 : f32
%4 = arith.addf %2, %3 : f32
memref.store %4, %arg2[%arg3, %arg4] : memref<4x4xf32>
}
}
}
return
}
}
and printing diffs between passes:
mlir-opt-repl> diff
--- --canonicalize
+++ --convert-linalg-to-loops
@@ -1,6 +1,21 @@
module {
func.func @matmul(%arg0: memref<4x8xf32>, %arg1: memref<8x4xf32>, %arg2: memref<4x4xf32>) {
- linalg.matmul ins(%arg0, %arg1 : memref<4x8xf32>, memref<8x4xf32>) outs(%arg2 : memref<4x4xf32>)
+ %c0 = arith.constant 0 : index
+ %c4 = arith.constant 4 : index
+ %c1 = arith.constant 1 : index
+ %c8 = arith.constant 8 : index
+ scf.for %arg3 = %c0 to %c4 step %c1 {
+ scf.for %arg4 = %c0 to %c4 step %c1 {
+ scf.for %arg5 = %c0 to %c8 step %c1 {
+ %0 = memref.load %arg0[%arg3, %arg5] : memref<4x8xf32>
+ %1 = memref.load %arg1[%arg5, %arg4] : memref<8x4xf32>
+ %2 = memref.load %arg2[%arg3, %arg4] : memref<4x4xf32>
+ %3 = arith.mulf %0, %1 : f32
+ %4 = arith.addf %2, %3 : f32
+ memref.store %4, %arg2[%arg3, %arg4] : memref<4x4xf32>
+ }
+ }
+ }
return
}
}
(which is actually colorized in a compatible terminal).
It also supports a rewind [N] for reverting passes:
mlir-opt-repl> rewind
Rewound 1 step(s). Now at: --canonicalize
module {
func.func @matmul(%arg0: memref<4x8xf32>, %arg1: memref<8x4xf32>, %arg2: memref<4x4xf32>) {
linalg.matmul ins(%arg0, %arg1 : memref<4x8xf32>, memref<8x4xf32>) outs(%arg2 : memref<4x4xf32>)
return
}
}
mlir-opt-repl>
Importantly (thanks @MaheshRavishankar for the idea
) everything available via repl is also available via mcp i.e., Claude can drive
; just add this to your .mcp.json:
{
"mcpServers": {
"mlir-opt-repl": {
"command": "mlir-opt-repl",
"args": ["mcp"],
"env": {"MLIR_OPT": "/path/to/build/bin/mlir-opt"}
}
}
}
Motivation
Understanding how MLIR passes transform IR typically involves running mlir-opt repeatedly with different flag combinations, manually diffing outputs, and losing track of intermediate states. This tool makes that workflow interactive and stateful since you can build up a lowering pipeline incrementally, inspect each step, and backtrack without starting over.
The MCP server mode allows AI coding assistants (Claude Code) to programmatically drive mlir-opt pipelines during development sessions, with the same history/rewind/diff capabilities.
Features
- Terminal REPL (default): load MLIR from files or stdin, apply passes one at a time, view colored diffs of what changed, rewind to try different lowering paths.
- MCP server: exposes the same functionality as a Model Context Protocol server over JSON-RPC stdio, for use as a Claude Code tool.
Stateful pass pipeline execution
run_pipeline/run: feed MLIR through passes, stores the resultchain_pipeline/run(subsequent): apply more passes to the current IR- State persists across calls; each pass application is recorded in history
History and rewind
- Full history of every pass application with the IR at each step
rewind N: undo the last N steps, restoring IR to that pointreset: clear everything and start fresh
Diff visualization
- Unified diff: standard
---/+++format showing what each pass changed - Side-by-side: two-column comparison with changed lines aligned
- Both support ANSI-colored “pretty” mode (red/green/dim) for terminal display
- Compare any two history steps by index, not just consecutive ones
Pass discovery
list_passes/passes [filter]: query availablemlir-optpasses with substring filtering
Distribution
I plan to develop it in-tree (assuming the community accepts) but also ship it to PyPI (regardless).
PR
PR over here. Feel free to suggest more features here or there.