Querying and editing MLIR from the command line
Pass a query followed by input paths. Each file is an independent MLIR document;
omitting paths reads stdin. The examples use fixtures from the
v0.7.0 checkout; run them from
its root with Zirium 0.7.0 installed. See getting started
for installation, or build the CLI in the checkout:
cargo build --bin ziriumTo use that local build, add target/debug to PATH:
export PATH="$PWD/target/debug:$PATH"The query language reference lists every predicate, pipeline stage, and output rule.
Inputs and reusable .zirium and .zirql programs are in
examples/cli. Short queries are shown inline.
The default baseline registry accepts ordinary, named, and nested module
shorthand. Use --preset stablehlo for a bundled dialect or --registry FILE
for a custom configuration. Both flags are repeatable. See
custom formats for details.
zirium --help lists options, and zirium --list-presets lists available
dialects. Recovery warnings on stderr mean semantic queries may be incomplete.
By default, reachable retains an operation with unknown reference semantics
as a leaf and continues elsewhere. Add --strict to reject either case in
scripts. Options can appear before or after the query argument. Use -- before
input paths beginning with a dash.
Shared patterns and predicates
Section titled “Shared patterns and predicates”Run the shared-language report against the existing small MLIR fixture:
zirium -f examples/cli/shared-predicates.zirql examples/cli/subtree-cost.mlirThe program destructures a list binding and tuple generators, compares common
for/in/if and alternate pipe/where comprehensions, then filters the tuples
with a named predicate. For a native ancestor query, use:
zirium 'filter(op("example.left")) | root(p => one(p | names) == "example.root") | names' examples/cli/subtree-cost.mlirThis prints example.root. The root predicate receives each candidate ancestor.
Troubleshoot source errors
Section titled “Troubleshoot source errors”Syntax and lowering failures are written to stderr with the input or query name, a one-based line and display column, a specific message, a bounded source excerpt, and a marker. For example, an unresolved operand looks like:
$ zirium count broken.mlirzirium: could not lower broken.mlir:broken.mlir:2:17: error: unresolved SSA value `%missing` "example.use"(%missing) : (i32) -> () ^~~~~~~~Inline queries use <query> as their source name; -f diagnostics use the
program path. Columns count Unicode characters rather than UTF-8 bytes. Invalid
UTF-8 bytes are shown as �, each invalid byte occupies one column, tabs
advance to four-column stops, CR in CRLF is omitted, and an error at end of file
gets a marker at the corresponding empty or final line. Source lines longer
than 120 display columns are clipped around the marker and marked with ….
The original byte ranges remain attached to parser and lowering diagnostics.
As with other failures before delivery, stdout stays empty.
Every CLI evaluation provides {document} as the supplied input path, or
stdin for standard input. Use it in report text or JSON strings:
zirium 'print("# {document}"); names | tally | markdown' first.mlir second.mlirFor machine-readable multi-file output, --jsonl writes one compact record
per emitted result with document and result fields. JSON results remain JSON
values; text and selected MLIR are strings. Records keep input and statement
order, and stdout remains empty if any input or emission fails. --ndjson is an
alias for --jsonl:
zirium --jsonl 'names | tally | json' first.mlir second.mlirFind untagged arithmetic
Section titled “Find untagged arithmetic”Boolean predicates can combine operation names and attributes. This command
prints the untagged arith.addi from the sample and omits its tagged
arith.muli:
zirium \ 'filter((op("arith.addi") or op("arith.muli")) and not has_attr("analysis.tag"))' \ examples/cli/arithmetic.mlirThe result contains the add and its enclosing module syntax. Its operand
definitions and users are omitted, so the fragment may not be valid on its own.
The same query is available as
untagged-arithmetic.zirium.
Find direct consumers
Section titled “Find direct consumers”users follows direct SSA uses. This sample’s add has one consumer, the multiply:
$ cat examples/cli/arithmetic.mlirmodule { %lhs = arith.constant 6 : i32 %rhs = arith.constant 7 : i32 %sum = arith.addi %lhs, %rhs : i32 %product = "arith.muli"(%sum, %rhs) {analysis.tag = "old"} : (i32, i32) -> i32}$ zirium 'filter(op("arith.addi")) | users' examples/cli/arithmetic.mlirbuiltin.module { %v3 = "arith.muli"(%v2, %v1) {analysis.tag = "old"} : (i32, i32) -> i32}The same query is available as
direct-consumers.zirium.
Combine arithmetic operation kinds
Section titled “Combine arithmetic operation kinds”union combines the results of two queries. The result contains the add and
multiply, following the left operand and then the right:
zirium \ 'filter(op("arith.addi")) union filter(op("arith.muli"))' \ examples/cli/arithmetic.mlirThis is also a selected fragment, so its SSA dependencies are omitted.
The same query is available as
arithmetic-union.zirium.
Extract a call dependency slice
Section titled “Extract a call dependency slice”fixpoint(closure) adds dependencies until the selection stops changing. For a
func.call, that includes the resolved callee and its body, and leaves
unrelated sibling functions out.
zirium 'filter(op("func.call")) | fixpoint(closure)' examples/cli/calls.mlirThe output contains @caller and @answer, and omits @unrelated. The slice
may still need other parts of the input to be valid MLIR.
The same query is available as
call-closure.zirium.
Count operations in a StableHLO decoder
Section titled “Count operations in a StableHLO decoder”The larger stablelm-decode.mlir
example represents one token-generation step for a small two-layer StableLM.
It contains rotary position handling, KV-cache updates, grouped-query
attention, gated MLPs, and a language-model head.
The example is adapted from MLXcel’s Apache-2.0 licensed StableLM decode program. Its large embedded rotary tables are replaced by zero splats; the operation structure and tensor shapes are unchanged.
The first query below counts every operation. The second counts reductions:
$ zirium --preset stablehlo --strict 'count' examples/cli/stablelm-decode.mlir237$ zirium --preset stablehlo --strict \ 'filter(op("stablehlo.reduce")) | count' examples/cli/stablelm-decode.mlir14The initial selection contains every operation, including the module and other container operations.
For a query that will be reused, put the program in a file. The checked-in
stablehlo-matmul-count.zirium
counts stablehlo.dot_general, the StableHLO operation used for the decoder’s
matrix multiplications:
$ cat examples/cli/stablehlo-matmul-count.ziriumfilter(op("stablehlo.dot_general")) | count$ zirium --preset stablehlo --strict \ --program-file examples/cli/stablehlo-matmul-count.zirium \ examples/cli/stablelm-decode.mlir19The nineteen operations comprise nine matrix multiplications in each decoder layer and one final projection to logits. A program file contains only Zirium source; surrounding whitespace and the final newline are ignored.
The count is structural: compact reductions using applies stablehlo.add do
not synthesize reducer-body operations. Use an explicit-region input if those
operations must participate in queries.
Inspect shapes and operation kinds
Section titled “Inspect shapes and operation kinds”Name and type projections make inventories easy to pipe into ordinary tools:
zirium --preset stablehlo --strict \ 'filter(dialect("stablehlo")) | names' examples/cli/stablelm-decode.mlir \ | sort | uniq -c | sort -nr
zirium --preset stablehlo --strict \ 'filter(op("stablehlo.dot_general")) | result_types | unique' \ examples/cli/stablelm-decode.mlirUse result_type("tensor<32xf32>") to select that exact result type, or json
to inspect operand types, result types, and complete dimension clauses together.
Paired custom dimensions such as [0] x [1] remain intact in attribute projection.
For a ranked JSON report of tensor shapes, logical sizes and input locations, run the Python tensor report against the same decoder fixture.
Inspect a single output computation
Section titled “Inspect a single output computation”The decoder returns logits, a key cache, and a value cache. Select the first return operand and follow its SSA definitions, stopping at function inputs:
zirium --preset stablehlo --strict \ 'filter(op("func.return")) | defs(0) | slice' \ examples/cli/stablelm-decode.mlirChange the index to 1 or 2 for either cache. slice does not expand symbols
or region bodies. At a multi-result operation it follows all explicit operands,
without inferring result-specific dependencies. The output remains an inspection
fragment. Use fixpoint(closure) when retaining scopes and supported callees is
more important than a narrow slice.
Keep operations satisfying a relationship
Section titled “Keep operations satisfying a relationship”Find matmuls that directly feed an addition by intersecting the matmul set with the definitions used by additions:
zirium --preset stablehlo --strict \ '(filter(op("stablehlo.dot_general")) intersect (filter(op("stablehlo.add")) | defs)) | json' \ examples/cli/stablelm-decode.mlirThis retains ten matmuls in the decoder. Use users(0) to follow only the first
result of a multi-result operation.
Tag selected operations
Section titled “Tag selected operations”Edits preserve the current selection. A do statement suppresses the edited
selection’s implicit output; the following emit starts from the whole edited
document and prints all operations once:
zirium \ 'do filter(op("arith.addi")) | set_attr("analysis.tag", "review"); emit' \ examples/cli/arithmetic.mlirThe output contains the complete document, with analysis.tag = "review"
added to arith.addi.
The same query is available as
tag-add.zirium.
Remove a tag while reading standard input
Section titled “Remove a tag while reading standard input”When no input path follows the query, the binary reads MLIR from standard input:
zirium \ 'do filter(string_attr_eq("analysis.tag", "old")) | remove_attr("analysis.tag"); emit' \ < examples/cli/arithmetic.mlirThe output retains the complete document but removes analysis.tag
from arith.muli.
The same query is available as
remove-tag.zirium.
Inspect an intermediate selection
Section titled “Inspect an intermediate selection”emit prints the selection and passes it to the next stage. This prints the
add, then its direct users, back-to-back:
zirium 'filter(op("arith.addi")) | emit | users' examples/cli/arithmetic.mlirZirium does not insert separators between statement or intermediate outputs.
Use print("text") when a report needs an explicit heading or delimiter.
Selection output includes enclosing operation shells. Use
--fragment-scope minimal when large incidental attributes on those ancestors
would dominate a small fragment. The selected operations and selected bodies
keep their attributes; enclosing shells retain structural identity and
signature metadata. Fragments may still lack SSA definitions, callees, or
terminators needed for valid standalone MLIR.
Work inside a function fragment
Section titled “Work inside a function fragment”subtree includes every descendant of the selected function. The following
filter therefore finds returns only inside @caller:
zirium \ 'filter(op("func.call")) | parent | subtree | filter(op("func.return"))' \ examples/cli/calls.mlirPer-function operation inventories
Section titled “Per-function operation inventories”Run the saved histogram query on the decoder:
zirium --preset stablehlo --strict \ -f examples/cli/function-op-counts.zirium examples/cli/stablelm-decode.mlirThe result is a JSON object keyed by function name, containing counts keyed by
operation name. In this example, main contains 19 stablehlo.dot_general
operations. Add reachable | before names to include supported callees’
bodies, counting shared operations once. See
bindings and aggregation for
the evaluation rules and the distinction between body and reachable counts.
For a Markdown report with an interpolated function count and explanatory text:
zirium --preset stablehlo --strict \ -f examples/cli/function-report.zirium examples/cli/stablelm-decode.mlirSee Markdown and text reports for supported table shapes and interpolation rules.
To package the histograms with metadata in one JSON document:
zirium --preset stablehlo --strict \ -f examples/cli/function-report-json.zirium examples/cli/stablelm-decode.mlirThe literal syntax preserves numeric counts and nested maps while allowing interpolated report labels.
Compare two documents
Section titled “Compare two documents”zirium --diff examples/cli/diff-before.mlir examples/cli/diff-after.mlirzirium --diff examples/cli/diff-before.mlir examples/cli/diff-after.mlir \ -f examples/cli/diff-report.ziriumThe first command prints a human summary. The second emits the changed arith
attribute records as zirium.diff.v1 JSON.
Function report rows
Section titled “Function report rows”The function fixture contains double,
mix and plain. A short correlated report needs only the existing query
argument:
zirium 'filter(op("func.func")) | project(f, { "name": f | attr("sym_name") | one, "n": f | children | count }) | json' examples/cli/function-report.mlirThe rows are double: 2, mix: 3, plain: 1, in input order. The
complete program filters rows by a
literal-bound minimum count and summarizes body and matching-operation counts:
zirium -f examples/cli/function-report.zirql examples/cli/function-report.mlirIts local operation_name = "arith.addi" and minimum_ops = 2 produce two
functions and totals 5/2. Passing the program text inline yields the same native
report and JSON. See the report language
and typed report builders for
cardinality, scope and budget rules.
Group functions by match count
Section titled “Group functions by match count”Group the same three functions by the number of represented arith.addi
operations. This complete short program uses literal arguments:
zirium 'let rows = filter(op("func.func")) | project(f, { "name": f | attr("sym_name") | one, "matches": f | children | subtree | filter(op("arith.addi")) | count});group_by(rows, "matches") | json;' examples/cli/function-report.mlirzirium -f examples/cli/function-groups.zirql examples/cli/function-report.mlirThe saved program returns the same JSON:
[ {"key": 1, "items": [{"name": "double", "matches": 1}, {"name": "mix", "matches": 1}]}, {"key": 0, "items": [{"name": "plain", "matches": 0}]}]Groups follow first occurrence, so key 1 precedes 0. Items retain function
order. Rust and Python callers use ReportExpr::from(rows).group_by("matches")
and E(rows).group_by("matches") respectively; the
builder guide describes their native
results. Grouping rules preserve the
existing rendering, diagnostic and budget boundaries.
Recursive containment cost
Section titled “Recursive containment cost”The tree fixture contains an unannotated
root and exactly two leaves with integer costs 3 and 4.
This inline function returns 8:
zirium 'fn subtree_cost(op: Operation) -> int { (cost_annotation(op) default 1) + sum([subtree_cost(child) for child in direct_children(op)])}let selected = filter(op("example.root"));report subtree_cost(selected) | json' examples/cli/subtree-cost.mlirThe saved program and Rust builder example run as follows:
$ zirium --program-file examples/cli/subtree-cost.zirql examples/cli/subtree-cost.mlir8$ zirium --program-file examples/cli/tail-accumulator.zirql examples/cli/subtree-cost.mlir500500$ cargo run -p zirium --locked --example typed_functions8500500The inline command and first saved command run the same
function program. direct_children
follows containment in source order; it does not traverse SSA users.
Missing cost selects default 1. A stored string cost is an error:
printf '"example.root"() {cost = "bad"} : () -> ()\n' | zirium --program-file examples/cli/subtree-cost.zirqlThis command fails with a located cost annotation must be an i64 integer
diagnostic and empty stdout. Changing the annotation to an integer outside i64
also fails. default never replaces malformed data or an exhausted allowance.
The accumulator performs 1,001 calls
under the finite default of 64 active activations. Compatible tail calls reuse
one activation; work and produced data remain bounded by --max-work and
--max-results. The compiled Rust example also sets an active limit of one.
See typed functions for signature
syntax, lexical scope, purity and independent limits.
Destructure report values
Section titled “Destructure report values”The saved destructuring program binds tuple and record fields, then passes a tuple to a typed function:
zirium -f examples/cli/destructured-report.zirql examples/cli/function-report.mlir{"summary":{"label":"alpha","total":5},"title":"operations","count":3}The guarded value match tests an optional lookup. Its false guards skip their bodies, then a local block computes the selected result:
zirium -f examples/cli/guarded-match.zirql examples/cli/function-report.mlir{"kind":"small","cost":5}Passing either program inline produces the same result. The CLI buffers all output until the whole program succeeds, including failures in a later match guard. See pattern rules and Rust and Python report builders.
Filter projected names with exact string matching
Section titled “Filter projected names with exact string matching”zirium 'let labels = filter(op("func.func")) | attr("sym_name"); [n for n in labels if contains(n, "i") and starts_with(n, "m") and ends_with(n, "x")] | json;' examples/cli/function-report.mlirzirium --program-file examples/cli/string-matching.zirql examples/cli/function-report.mlirThe inline query returns ["mix"]. The saved program adds Unicode and empty
search controls. Matching is case-sensitive and does not normalize Unicode or
interpret regex. All three helpers require strings; empty searches match.
Inline and file programs share located errors, the query budget and output
buffering when a later expression fails. Rust and Python callers use the
same helpers on ReportExpr.