Structured queries in Rust and Python
These examples use Zirium 0.7.0. Repository-relative inputs are in the
v0.7.0 checkout;
see getting started for installation.
Build queries with methods and predicates, then evaluate them against a semantic document. Rust and Python use the same expression tree and return native values. Queries are read-only; use the textual language for CLI queries and edits.
Textual positional comprehensions
construct native Zirql values from explicit scalar projections. Rust callers
can match NativeValue::Constructed(report) and inspect report.value() in a textual Query
callback before rendering. The typed Rust and Python builders keep their
existing operation handles, strings, counts, and maps. The
report builders also construct shared values,
comprehensions and correlated rows. Native capture builders
retain operations and exact SSA values before an explicit scalar projection.
Start a query
Section titled “Start a query”In Python, Document.query() returns the result described by the expression:
import ziriumfrom zirium.query import ops, op
source = '''%c = "example.make"() : () -> i32"example.consume"(%c, %c) : (i32, i32) -> ()'''lowered = zirium.parse_text(source).lower_strict()assert lowered.document is not None, lowered.diagnosticsdocument = lowered.document
consumers = ops().filter(op("example.make")).users().unique()selected = document.query(consumers) # list[SemanticOperation]count = document.query(consumers.count()) # intassert [operation.name for operation in selected] == ["example.consume"]assert count == 1The equivalent Rust expressions use the same methods:
use zirium::query::{ops, op};
let consumers = ops().filter(op("example.make")).users().unique();let selected = document.query(&consumers)?; // Vec<OperationId>let count = document.query(&consumers.count())?; // usizeExpressions are immutable and reusable. Each document.query() call evaluates
the expression against that document’s current state.
Rust Document::query() uses the baseline registry. For other dialect semantics,
use document.query_with_registry(&expression, ®istry), passing the registry
used to parse and lower the document. Python retains that registry in the document
and uses it automatically.
Required scalar inputs
Section titled “Required scalar inputs”A typed request carries root declarations through composition, erasure and
conversion to a report expression. Attach declarations with with_parameters
and supply native scalar values at evaluation:
use zirium::query::{ConstructedValue, EvaluationLimits, Located, ParameterDeclaration, ParameterType, ReportExpr, report};
let query = report(ReportExpr::name("minimum_ops")).with_parameters(vec![ ParameterDeclaration { name: Located::new("minimum_ops".to_owned(), 0..0), ty: Located::new(ParameterType::Int, 0..0), },]);let result = query.evaluate_with_parameters( &document, ®istry, &[("minimum_ops".to_owned(), ConstructedValue::Integer(2))], EvaluationLimits::default(),)?;assert_eq!(result.value(), &ConstructedValue::Integer(2));Root declarations are visible throughout the builder request. Identical
metadata shared by composed expressions is retained once; conflicting
metadata reaches admission and fails. A local scope or projection cannot
shadow a root declaration. op_expr(ReportExpr::name("operation_name"))
checks a supplied string. Rust also has head_expr, tail_expr, attr_expr,
defs_at_expr and users_at_expr for shared scalar expressions.
Python report requests use the same declarations and admission:
from zirium.query import ReportExpr, report
query = report(ReportExpr.name("minimum_ops")).with_parameters( {"minimum_ops": "int"})assert document.query(query, parameters={"minimum_ops": 2}) == 2Declaration types are "int", "string" and "bool"; inputs are actual Python
int, str and bool values. Booleans remain Boolean values rather than integer
inputs. Integers must fit signed 64 bits; overflow raises OverflowError.
Unsupported value kinds raise TypeError. Strings remain exact data.
Conversion, admission, evaluation and native result conversion spend the same
request allowance, including input names and string bytes.
Every declaration needs one input; missing or extra inputs and type mismatches
fail before native document work. Existing evaluation methods supply an empty
input set. For parsed Rust requests, Query::parameters() exposes declarations,
Query::evaluate_with_parameters accepts typed scalar pairs, and
evaluate_with_parameters_and_budget accepts ParameterInputs::Typed or
ParameterInputs::Cli with the caller’s budget. Library emission callbacks
retain their existing behavior; callers buffer emissions when a later failure
must discard earlier output. The CLI already buffers complete requests.
Text queries also accept shared predicate arguments, for example
filter(p => one(p | names) == "example.make") and
filter(one(_ | names) == "example.make"). The
predicate reference covers
constructed filters, destructuring and native candidate scopes. Rust and
Python builders keep their typed native predicates; textual arguments require
no host-language callable or closure.
Compose predicates and streams
Section titled “Compose predicates and streams”Available predicates are op(name), dialect(name), has_attr(name),
result_type(spelling), string_attr_eq(name, value), and always(bool).
Combine them with & and |; negate with ! in Rust and ~ in Python:
from zirium.query import has_attr
arithmetic = ops().filter( (op("arith.addi") | op("arith.muli")) & ~has_attr("skip"))Python’s and, or, and not do not build predicates. Converting a predicate or
query expression to bool raises TypeError with guidance on the supported
operators. String arguments are data; they need no query-language escaping.
Navigation methods include defs, users, parent, children, root(predicate),
subtree, slice, backward_slice, forward_slice, closure, and reachable.
slice() is an alias for backward_slice(). Rust has defs_at(index) and
users_at(index) for indexed navigation; Python accepts defs(index) and
users(index). Indices select the current operation’s operand or result,
respectively.
Navigation preserves order and duplicates. A consumer using the same result twice
appears twice in users(). Add unique() when counting distinct operations.
union(other), intersect(other), and difference(other) combine operation or
string queries. Union keeps first appearance from the left operand, then the right.
Intersection and difference deduplicate in left-operand order. Each operand is
a complete expression evaluated with the same input.
head(n), tail(n), and reverse() apply to streams and dictionaries;
sort() orders strings or dictionary keys.
These operate on computed streams and do not avoid parsing the document or
executing earlier stages.
Graph traversal has the same structural meaning and dialect limitations as the
textual language. In particular, closure() is one scope-retaining dependency
step; backward_slice() follows transitive SSA definitions,
forward_slice() follows transitive SSA users, and reachable() also follows
supported calls and branches. For repeated closure, use:
from zirium.query import input
fragment = arithmetic.fixpoint(input().closure())A general fixpoint(body) replaces its selection until the ordered stream is
unchanged. It preserves duplicate semantics, detects cycles, and obeys work and
item limits. For accumulating transitive users, use
arithmetic.fixpoint(input().union(input().users())).
MapQuery returns ordered Vec<(String, T)> entries in Rust and a Python
dict in insertion order. sort_by_value() stably orders ordinary data values;
sort() orders keys. Both return a dictionary query, as do reverse(),
head(n) and tail(n). Equal values retain their current order, including
after reversal. head_expr and tail_expr accept a ReportExpr bound.
reachable() treats an unregistered operation as a retained leaf by default;
it does not follow that operation’s regions, operands, successors, or symbol
references. Other queued operations are still processed. Pass strict=True to
Document.query to reject such an operation by name. Known malformed calls,
missing or external callees, and unsupported registered references remain
errors. closure() and fixpoint(input().closure()) always use strict
reference semantics.
Nested queries and native maps
Section titled “Nested queries and native maps”ops() always starts from every operation in the document. input() starts from
the selection supplied to a nested query. At top level, both start from all
operations. In map_by, sort_by, and where_exists, the nested input is one
operation at a time.
This query maps each function name to a histogram of its represented body:
from zirium.query import input
functions = ops().filter(op("func.func"))report = functions.map_by( key=input().string_attr("sym_name").one(), value=input().children().subtree().names().tally(),)histograms = document.query(report) # dict[str, dict[str, int]]Rust uses references for nested expressions:
use zirium::query::input;
let functions = ops().filter(op("func.func"));let report = functions.map_by( &input().string_attr("sym_name").one(), &input().children().subtree().names().tally(),);let histograms = document.query(&report)?; // Vec<(String, Vec<(String, usize)>)>one() requires exactly one string and returns a scalar. Missing or multiple
strings are errors. map_by rejects duplicate keys. String tally() combines equal strings and returns their occurrence counts
in first-occurrence order. Operation, type and attribute queries also expose
tally(), returning retained dictionaries through the existing erased Rust
QueryOutput. Python uses the existing MapQuery wrapper. Non-string keys
convert to tagged entry pairs containing actual native values; string keys
keep ordinary dictionaries.
map_by retains input order. Empty inputs produce empty maps.
Map values can also be operation selections, type or attribute selections, strings, scalar strings, counts, or nested maps. Operations remain usable handles; there is no conversion through JSON. Build larger reports with normal Python or Rust data structures.
To retain operations based on a relationship:
matmuls = ops().filter(op("stablehlo.dot_general"))with_add_users = matmuls.where_exists( input().users().filter(op("stablehlo.add")))where_exists accepts an operation query and keeps an input operation when that
query returns a nonempty selection. It preserves the original operations,
order and duplicates. A complete empty relation rejects its subject; unresolved
relations and nested errors fail. The text form is where_exists(users | filter(op("stablehlo.add"))). Relations are pure and are checked even when the
outer input is empty.
sort_by, min_by, max_by, min_all_by, and max_all_by accept scalar string
or count queries. For example, functions.sort_by(input().children().count())
sorts by direct child count. Sorting is stable; single extrema retain the first
tie, and all-extrema retain every tie. Empty extrema retain an empty native
selection with its admitted kind, element declaration and owner.
Projections and editing
Section titled “Projections and editing”| Expression | Rust result | Python result |
|---|---|---|
| Operation query | Vec<OperationId> |
list[SemanticOperation] |
names() |
Vec<String> |
list[str] |
string_attr(name) |
Vec<String> |
list[str] |
result_types(), operand_types() |
Vec<TypeId> |
list[SemanticType] |
attributes(name) |
Vec<AttributeId> |
list[SemanticAttribute] |
String query followed by one() |
String |
str |
count() |
usize |
int |
tally() |
Vec<(String, usize)> |
dict[str, int] |
Type and attribute queries expose spellings() for retained MLIR text.
string_attr(name) decodes string attributes, omits missing attributes, and
reports an error for a present non-string attribute or undecodable UTF-8.
Use attributes(name).spellings() to inspect those retained spellings.
For example, a configured call target is a symbol-reference attribute, so read its retained MLIR spelling rather than treating it as a string attribute:
calls = ops().filter(op("vendor.invoke"))targets = document.query(calls.attributes("target").spellings())calls.string_attr("target") is appropriate only when target is an MLIR
string attribute; it reports an error for a symbol reference such as @callee.
Rust exposes methods according to the result type: a string query has no users
method. Python exposes corresponding classes and type declarations, including
QueryExpr[T] for annotating reusable expressions.
Evaluated selections keep their membership, while their handles refer to live objects and reflect later edits. Accessing deleted or invalidated objects raises a stale-handle error. Scalars and strings keep their evaluated values.
Select first, then use the existing edit transaction:
selected = document.query(ops().filter(has_attr("analysis.tag")))with document.edit() as edit: for operation in selected: edit.remove_attribute(operation, "analysis.tag")Limits and errors
Section titled “Limits and errors”Python accepts per-evaluation limits:
selected = document.query( consumers, max_work=1_000_000, max_items=100_000, strict=True)Rust accepts them on the expression:
use zirium::query::{EvaluationLimits, EvaluationOptions};
let selected = consumers.evaluate_with_options( &document, ®istry, EvaluationOptions { strict_unknown_references: true }, EvaluationLimits { max_work: 1_000_000, max_items: 100_000 },)?;The defaults are 10 million work units and 1 million items. Limits cover expression nodes, scope and type preparation, stage inputs, visited predicate nodes, traversal work, and nested result sizes. They are not byte or wall-clock bounds. Boolean evaluation short-circuits; skipped branches consume no evaluation work.
Nested expressions and predicates have a 64-level limit; flat pipelines, boolean chains, and set chains do not consume a level per term. Excessive nesting is reported when the expression is evaluated.
Ordinary and diff programs use Zirql’s statement order, immutable bindings,
groups, sets and composition steps. Ordinary text
and diff text programs use zirql::read_program for function collection,
parameters, pattern lets and separators. The ordinary adapter preserves the
public parser’s statements, final expression and parameter positions; typed
builders continue to construct that frontend model directly. Zirium retains
legacy bindings, do and native output policy.
The legacy binding reader accepts a single = and leaves == to the
expression reader. Queries parsed with Query::parse("let n=1; n == 1") or
Query::parse("let n=1; report n == 1") both evaluate to the Boolean true.
Filter, match, ordering, mapping, existence, fixed-point and path bodies prepare
through the shared runner. Each selector and existence relation receives one
subject; a fixed point receives the complete
selection. Match clauses keep their original input and earlier named captures.
Native hooks retain MLIR identity, result shape and the diff side. Typed diff
fixed-point bodies and navigation use the same active allowance. The textual
Query API also accepts evaluate_with_program_limits;
--max-results exposes its aggregate allowance
in the CLI. Typed expression APIs retain their existing limits and native values.
See query language limits for the
aggregate units and the caller-owned printing boundary.
Text parsing returns query::QueryError with an owned String message and a
query byte position. query::parser::Diagnostic also owns its message; it is
Clone and no longer Copy. Its message(&self) -> &str and range(&self)
accessors borrow the diagnostic. Direct QueryError construction supplies a
String, for example message: "invalid query".into(). Ownership lets detailed
shared errors survive parsing without replacing them with a generic message.
Evaluation returns EvaluationError in Rust and raises ValueError in Python.
Python rejects invalid argument types with TypeError. Evaluation runs in Rust
with shared document access and without holding the Python interpreter lock;
queries cannot contain Python callbacks, edits, or emitters.
Typed diff builders start with changes(). A ChangeQuery accepts ordinary and
change predicates; .before() and .after() return side-scoped operation
queries. Rust returns DiffOperationSelection rather than bare operation IDs.
Python resolves those selections to wrappers backed by immutable snapshots.
Use diff_op_input() to build a relative fixed-point body, for example
changed_ops.fixpoint(diff_op_input().slice()). Diff value projections include
names(), attr(...), result_types(), and operand_types().
DiffProgram::evaluate_with_parameters accepts typed scalar inputs;
evaluate_with_parameters_and_budget accepts zirql::ParameterInputs and one
request budget. parameters() exposes the parameter declarations. Native function
signatures use Change, BeforeOperation and AfterOperation, preserving
comparison and side admission. See semantic diff for a
parameterized text report and the native builder examples.
Constructed report builders
Section titled “Constructed report builders”OpQuery::project returns a ReportQuery whose native result is
ConstructedReport. bound_ops names a saved native selection or a projection
subject. ReportExpr contains the shared value tree; construction reads no
document and accepts no query-source strings or evaluator callbacks.
Textual reports and typed builders produce Zirql’s located Value,
Projection and composition steps directly. Each request registers its functions with one
PreparedFunctions unit, including requests with no function declarations.
prepare_expression checks the written bodies and retains their result kinds;
evaluate_expression supplies the current bindings, relative input and cumulative
budget. Zirium’s native stages retain their MLIR payloads and ownership checks.
Native tuples, capture records and positional sequences expose ordinary data to the same infix admission as constructed report values. Equality checks compatible underlying types before returning an unequal result; relations require numbers. Explicit optional and empty sequence declarations survive native wrapping. Admission of deferred values occurs only when that comparison executes. MLIR handles retain exact native identity and require the existing explicit scalar projections where needed.
Projection fields are checked before enumerating subjects, so an empty selection retains its declared row schema and still rejects invalid fields. The shared projection contract owns field scope and scalar construction; the builder below supplies the native operation selection.
The ordinary textual report frontend projects saved scalar streams and counts
through NativeQuery. Function-capable bodies use NativeValue to retain
admitted handles as well as scalar results. Both forms execute in the same
prepared unit; report-only positions still require an explicit scalar projection
of native handles.
Core collection steps handle count, bounds, unique and tally, and core
ordering handles sort. They preserve the admitted native or constructed shape.
Zirium supplies exact operation, type and attribute identities and retains
empty native selections with their admitted kind and element declaration. Native navigation keeps its document ownership
checks. A plain native count or selection keeps its work and stream limits;
attaching a prepared unit does not require an aggregate result allowance.
use zirium::query::{ReportExpr, bound_ops, op, ops};let f = bound_ops("f");let rows = ops().filter(op("func.func")).project("f", [ ("name", ReportExpr::from(f.attr("sym_name").one())), ("operations", ReportExpr::from(f.children().subtree().count())), ("matches", ReportExpr::from(f.children().subtree().filter(op("arith.addi")).count())),]);let native_report = document.query(&rows)?;let value = native_report.value();StringQuery, scalar-string queries, CountQuery and ReportQuery convert explicitly
to ReportExpr. Scalar literals, arrays, records, field/index access, generator
clauses, reducers and requirements all use the shared evaluator. ReportGenerator
constructs in or immutable binding clauses. ReportExpr::scope binds values
in order and returns one head through these same clauses. op_expr accepts a
checked string expression while the ordinary op("name") builder remains
available.
Python converts constructed lists, tuples and sets to ordered lists. Ordinary
string-key dictionaries become Python dictionaries; general dictionaries use
{"kind": "dictionary", "entries": [{"key": key, "value": value}]} so nested
collection keys do not require Python hashability. The Rust value retains its
collection kind until conversion. The Python builders retain their existing
constructors; general set and dictionary literals are available through the
textual language and the shared Rust value API.
attr(name) follows the textual attribute-spelling projection; string_attr(name)
continues to require and decode a string attribute.
Typed constructed requests select aggregate limits automatically.
evaluate_with_program_limits accepts explicit limits, and
evaluate_output_with_budget retains the caller’s remaining allowance for
conversion. Native handles stay document-owned; report values carry their query
location. Render explicitly with ConstructedReport’s budgeted JSON methods,
or call QueryOutput::to_json_with_budget(document, budget) for mixed native
reports. Both use Zirql’s shared ordinary codec and container traversal. Nominal
members and keyed arrays use tagged fallback, retaining member names and complete
axes. Native descriptors and document envelopes remain Zirium projections.
Python converts constructed data through that same codec to ordinary Python
lists, dictionaries and scalars; native operation, SSA, type and attribute results
retain their document-owning wrappers.
Python mirrors this surface through zirium.query:
from zirium.query import ReportExpr as E, bound_ops, op, ops, report, requiref = bound_ops("f")rows = ops().filter(op("func.func")).project("f", { "name": f.attr("sym_name").one(), "operations": f.children().subtree().count(), "matches": f.children().subtree().filter(op("arith.addi")).count(),})result = document.query(report(require(True, E(rows), "stop")))Document.query returns Python bool/int/float/string/list/dict data directly,
with record insertion order. ReportExpr also accepts scalar-query wrappers and
nested list/dict construction. ReportGenerator and ReportExpr.comprehension
supply value generators; .sum(), .length(), .forall() and .exists()
reduce arrays. ReportExpr::group_by("field") in Rust and .group_by("field")
in Python group positional records through the shared evaluator. Groups contain
key and items, with first-seen keys and original row order and duplicates.
The grouping language reference defines
supported keys and failures.
Reuse the function rows above, whose matches field counts arith.addi
operations. Rust returns a native ConstructedReport:
let grouped = document.query(&zirium::query::report(ReportExpr::from(rows).group_by("matches")))?;let value = grouped.value();The corresponding Python call returns ordered list/dict data:
result = document.query(report(E(rows).group_by("matches")))With the function fixture,
both obtain key 1 with double/mix, then key 0 with plain.
Python conversion spends the remaining request budget before
allocating collections. Ordinary operation/type/attribute results retain their
existing semantic wrappers.
Transform report data
Section titled “Transform report data”The methods in this section accept shared expressions and patterns; they do not execute Rust or Python callbacks. Functions and parameter declarations compose with the existing report metadata.
import ziriumfrom zirium.query import ReportExpr as E, ReportGenerator as G, ReportPattern as Pfrom zirium.query import op, ops, report
lowered = zirium.parse_text('"example.seed"() : () -> ()').lower_strict()assert lowered.document is not Nonedocument = lowered.documentn = E.name("n")selected = E.comprehension( n, [G.in_pattern(P.present(P.name("n")), [None, 1, 2], case=True)])assert document.query(report(selected.map(P.name("n"), n + 1).windows(2))) == [[2, 3]]assert document.query(report(E([1, 2]).flat_map(P.name("n"), [n, n]))) == [1, 1, 2, 2]
rows = E([{"id": 1}, {"id": 1}, {"id": 2}])assert document.query(report(rows.runs_by("id").map( P.name("run"), E.name("run").field("items").length()))) == [2, 1]assert document.query(report(E({"id": 1, "amount": 2}).with_fields({"amount": 3}))) == { "id": 1, "amount": 3}assert document.query(report(E([{"id": 1}]).align([{"id": 2}], "id").map( P.name("entry"), E.name("entry").field("key")))) == [1, 2]
native_rows = E([{"operation": E.native(ops().filter(op("example.seed"))), "amount": 1}])result = document.query(report(native_rows.map( P.name("row"), E.name("row").with_fields({"amount": 2}))))assert result[0]["operation"].name == "example.seed"assert result[0]["amount"] == 2ReportExpr.native(query) retains native operations, SSA values or captures
without converting them to JSON. Python results keep document-owning wrappers.
An ordinary native selection keeps its existing kind; constructing an array of
native records produces positional report data. Rust exposes the same methods:
map(pattern, body), flat_map(pattern, body), windows(size), runs_by(field),
with_fields(fields) and align(after, field). ReportGenerator::in_pattern
accepts a located Pattern, a source, an optional filter and the selective
case flag. Existing in_values generators remain strict.
Zirql enumerates children and maps payloads. Zirium’s normalization callback handles retained expression wrappers; native-root discovery decomposes only host stages. Native type conversion, operation ownership, coordinates and read-only admission remain in Zirium.
Bounded native path builders
Section titled “Bounded native path builders”The typed builder uses the same traversal as the short command:
use zirium::query::{ops, op, PathEdge};
let paths = ops().filter(op("arith.muli")) .paths(PathEdge::Users, op("func.return"), 2);let native = document.query(&paths)?;paths returns CaptureQuery; native is CaptureMatches. On
examples/cli/function-report.mlir, its one row retains the native mix
operations [arith.muli, arith.addi, func.return]. The row’s source and sink
are singleton selections, and steps is NativeValue::OperationSequence.
It remains a sequence for a zero-edge witness. Native IDs and source ranges
remain available before rendering.
Python supplies the supported edge name explicitly:
from zirium.query import ops, op, capture_ops
paths = ops().filter(op("arith.muli")).paths( "users", to=op("func.return"), max_depth=2)native = document.query(paths)rows = document.query(paths.project("p", {"steps": capture_ops("p", "steps").names()}))Python path rows are dictionaries with native SemanticOperation endpoints
and a list of native SemanticOperation steps. Those wrappers retain their
document owner. Ordinary singleton capture fields keep their existing wrapper,
including fields named steps. Negative or overflowing Python bounds fail
before document evaluation.
The target accepts ordinary predicates. Root parameter declarations survive
path reuse, erasure, nesting and projection; dynamic target arguments use the
existing typed parameter admission. PathEdge::Users enumerates direct SSA
users in result/use-site order. The bound counts edges, matching roots produce
[source], and equal-length alternatives follow that order. No witness within
the bound does not prove global disconnection.
Paths use the active aggregate budget for traversal, copies, projection and
Python conversion. Library callers can retain an explicit
Budget::for_program(ProgramLimits { ... }); EvaluationLimits keeps its
per-stream and work ceilings. Paths remain read-only and are unavailable on diff
queries.
Order constructed values
Section titled “Order constructed values”ReportExpr::sort(descending) and sort_by(field, descending) stably order
positional scalars or records. Pass false for ascending and true for
descending; ties keep input order in both directions. Python provides the same
methods with keyword-only descending=False. The four extrema methods are
.min(), .max(), .min_by("field") and .max_by("field"). Empty input returns
absence (None in Python), and the first tied input wins. Original rows stay intact; the builders retain function and parameter
declarations.
use zirium::query::{ReportExpr as E, report};let rows = E::array([ E::record([("id", E::string("A")), ("score", E::integer(2))]), E::record([("id", E::string("B")), ("score", E::integer(1))]),]);let ordered = report(rows.clone().sort_by("score", false));let best = report(rows.max_by("score"));let empty_default = report(E::array([]).min().default(E::integer(0)));from zirium.query import ReportExpr as E, reportrows = E([{"id": "A", "score": 2}, {"id": "B", "score": 1}])ordered = report(rows.sort_by("score", descending=True))best = report(rows.max_by("score"))empty_default = report(E([]).min().default(0))The shared ordering entry
defines structural keys, selector errors, empty results and ties. Use an explicit
lookup/default field when missing storage should supply a key. Native OpQuery
and map builders retain their existing selector API and document ownership.
The MLIR recipe
runs the same constructed report through the text frontend.
Optional report values
Section titled “Optional report values”ReportExpr::null() constructs absence in Rust. The direct .occurs(),
.absent(), .lookup(key), .has_key(key) and .default(fallback) methods
build the same nodes as the textual language. They retain required parameter
metadata from both operands, including skipped fallbacks.
use zirium::query::{ReportExpr as E, report};
let info = E::record([("id", E::integer(4)), ("stored", E::null())]);let query = report(E::record([ ("value", info.clone().lookup(E::string("missing")).default(E::integer(8))), ("stored", info.clone().has_key(E::string("stored"))), ("info", info),]));let result = document.query(&query)?;Python accepts None in report scalars, lists and dictionaries and returns it
for stored absence. The corresponding methods accept ordinary report inputs:
from zirium.query import ReportExpr as E, report
info = E({"id": 4, "stored": None})result = document.query(report({ "value": info.lookup("missing").default(8), "stored": info.has_key("stored"), "missing": info.has_key("missing"), "info": info,}))assert result == { "value": 8, "stored": True, "missing": False, "info": {"id": 4, "stored": None},}Lookup misses return absence, while wrong key/container types and strict missing
access still fail. Default evaluates its fallback only for absence; zero,
false and empty containers remain present. Scope, purity and known-type checks
still cover both operands. See the textual optional report
for inline/file usage and the reusable report fixture with a native projection.
Required external parameters continue to accept int/string/bool only; passing
None as a parameter fails before execution. Optional report values add no new
MLIR semantics or edit format.
Native capture builders
Section titled “Native capture builders”SsaQuery and CaptureQuery retain native SSA values and capture rows.
OpQuery::operand returns one exact ValueId; CaptureQuery returns CaptureMatches, whose
rows() expose ordered native records and whose range() retains the query
location. Neither result is a constructed scalar value.
Build the native capture example with concrete immutable clauses:
use zirium::query::{bound_ops, bound_ssa, capture_ops, matches, op, ops, same_value, ReportExpr as E, report};
let captures = matches() .from("add", &ops().filter(op("arith.addi"))) .bind("lhs", &bound_ops("add").operand(0)) .bind("rhs", &bound_ops("add").operand(1)) .where_(&same_value(&bound_ssa("lhs"), &bound_ssa("rhs"))) .build();let native = document.query(&captures)?;
let add = capture_ops("m", "add");let rows = captures.project("m", [ ("function", E::from(add.root(op("func.func")).attr("sym_name").one())), ("operation", E::from(add.names().one())),]);let full = report(E::scope( [("rows", rows.into())], E::record([ ("matches", E::name("rows")), ("count", E::name("rows").length()), ]),));let result = document.query(&full)?;On the original function fixture, native contains one row with fields add,
lhs and rhs; the SSA handles are equal. result contains only the double
addition and count 1. bound_ops and bound_ssa refer to earlier captures.
capture_ops(subject, field) and capture_ssa(subject, field) select a field
from a projection’s native record and check its promised kind, even when the
capture collection is empty. Match guards combine with &, | and ! in Rust.
Python mirrors these builders with .from_ and .where_:
from zirium.query import ( ReportExpr as E, bound_ops, bound_ssa, capture_ops, matches, op, ops, report, same_value,)
captures = ( matches() .from_("add", ops().filter(op("arith.addi"))) .bind("lhs", bound_ops("add").operand(0)) .bind("rhs", bound_ops("add").operand(1)) .where_(same_value(bound_ssa("lhs"), bound_ssa("rhs"))) .build())native = document.query(captures)assert native[0]["lhs"] == native[0]["rhs"]
add = capture_ops("m", "add")rows = captures.project("m", { "function": add.root(op("func.func")).attr("sym_name").one(), "operation": add.names().one(),})full = report(E.scope( {"rows": rows}, {"matches": E.name("rows"), "count": E.name("rows").length()},))result = document.query(full)Python returns a list of dictionaries in capture-field order. Operation fields
are SemanticOperation wrappers and SSA fields are existing SemanticValue
wrappers, retaining their owning document. A standalone SsaQuery returns one
SemanticValue. Match guards combine with &, | and ~; converting one to
bool raises TypeError.
Capture builders retain root parameter declarations through reuse, erasure,
nesting and projection. Attach declarations with CaptureQuery.with_parameters
and supply values through the existing evaluation methods. Parsing, matching,
native copies, scalar projection and Python conversion spend one request
allowance. Invalid scope, native types, fields and effects fail before
enumeration, including for empty sources and unchosen Boolean branches.
operand requires one operation and a resolved operand; same_value compares
the full result or block-argument identity rather than its defining operation.
Native captures remain read-only and are unavailable on the diff surface.
Destructuring reports
Section titled “Destructuring reports”Rust exposes shared Pattern, PatternKind and Located values.
ReportExpr::tuple preserves tuple shape; ReportExpr::block takes ordered
(Pattern, ReportExpr) bindings and a final value. ReportExpr::value_match
takes a subject and ReportMatchArm { pattern, guard, value } arms.
FunctionDefinition::with_patterns accepts PatternParameter declarations;
FunctionDefinition::new retains its name-only parameter signature. The
complete Rust example uses a
tuple parameter and a tail call through a local block and value match.
Python’s report builders use reusable ReportPattern and ReportMatchArm
owners. They accept the existing report inputs and run through Document.query:
from zirium.query import ReportExpr as E, ReportPattern as P, ReportMatchArm as Armfrom zirium.query import report
cost = E.name("cost")query = report(E.value_match(E({"cost": 4}).lookup("cost"), [ Arm(P.absent(), {"kind": "absent", "cost": 0}), Arm(P.present(P.name("cost")), {"kind": "large", "cost": cost}, guard=cost > 10), Arm(P.present(P.name("cost")), E.block( [(P.name("answer"), cost + 1)], {"kind": "small", "cost": E.name("answer")}, )), Arm(P.wildcard(), {"kind": "fallback", "cost": 0}),]))assert document.query(query) == {"kind": "small", "cost": 5}P.tuple([...]), P.record({...}, ignore_rest=True), P.literal(value) and
E.tuple([...]) cover the same structural forms as the text reader.
pattern.located(start, end) retains source coordinates for diagnostics.
E.conditional(condition, selected, otherwise) builds a conditional value.
Patterns, arms and expressions refuse Python truth-value conversion. Queries
can be reused against another current document; they retain no active budget.
Python has no function-definition builder or textual-program entry point.
The language guide describes exact shapes, scope and ordered guards. All branches receive static checks, while runtime evaluation visits only demanded values. Native capture enumeration and operation/SSA ownership keep their existing boundaries.
Building typed functions in Rust
Section titled “Building typed functions in Rust”Use Zirium’s builders below to define and call typed functions while retaining checked MLIR operation handles. The Zirql embedding guide provides more detail on the shared value, pattern and function nodes.
FunctionDefinition uses shared FunctionParameter, FunctionType and value
nodes. It constructs declarations directly; no generated source is parsed.
Attach all definitions to the submitted QueryExpr with with_functions.
Composition shares identical definitions and retains conflicting definitions
for preflight to reject.
use zirium::query::{ FunctionDefinition, FunctionParameter, FunctionType as T, FunctionLimits, Located, ReportExpr as E, ValueBinaryOperator as B, report,};let parameter = |name: &str| FunctionParameter { name: Located::new(name.to_owned(), 0..0), declaration: T::Integer,};let accumulate = FunctionDefinition::new( "accumulate", vec![parameter("n"), parameter("total")], T::Integer, vec![], E::conditional( E::name("n").binary(B::Equal, E::integer(0)), E::name("total"), E::call("accumulate", [ E::name("n").binary(B::Subtract, E::integer(1)), E::name("total").binary(B::Add, E::name("n")), ]), ),)?;let query = report(E::call("accumulate", [E::integer(1000), E::integer(0)])) .with_functions(vec![accumulate]) .with_function_limits(FunctionLimits { active_depth: 1 });Run the complete library example:
cargo run -p zirium --locked --example typed_functionsIt evaluates the same native tree as the CLI, then the accumulator, and prints
8 and 500500. Evaluation and explicit JSON conversion use one budget.
FunctionType::Native(FunctionNativeType::Operation) and SSAValue describe
checked document-owned IDs. E::native(&query) retains a typed query operand;
E::operation(id) and E::ssa_value(id) supply explicit handles checked on
function admission. E::operation_sequence(ids) preserves sequence shape,
including zero or one operation. E::direct_children and E::cost_annotation
construct ordinary Call nodes using the same native definitions as the textual reader. function_call::<T>
starts a typed native query through the existing QueryResult conversion;
report(E::call(...)) returns ConstructedReport for scalar/report values.
Functions never retain the document, caller bindings, result history or active
budget. Zirql’s PreparedProgram prepares the canonical lowered statements and
all function bodies before execution. It advances parameter and binding scope,
then runs entries against the request’s current context. Relative native groups
retain their native result shapes and enclosing effect allowance. Zirium retains
MLIR kind admission, document ownership, reserved native names and emission.
Native callback arguments
retain a shared owner after a function frame returns. Passing or inspecting
unchanged constructed data shares its immutable owner; constructing a new row
or rendering output still pays for the produced data. External parameters keep
their existing int/string/bool transport. Python keeps its existing query builders and native result conversion; there
is no Python function builder API.
Building native worklists in Rust
Section titled “Building native worklists in Rust”ReportExpr::loop_, break_ and continue_ construct shared control nodes.
fold and scan take items, initial state and a named step.
queue, typed_queue, pop_front, push_back_all, identity_set, is_in
and insert expose the same computational operations as the textual reader.
Use FunctionType::Queue(Box::new(element)) and
FunctionType::IdentitySet(FunctionNativeType::Operation) in signatures.
typed_queue takes a SignatureType for an explicit empty element
contract. All builders merge their operands’ function and parameter metadata.
The native convenience builders direct_children, cost_annotation,
same_operation and user_info use ReportExpr::call; both arguments of
same_operation contribute metadata and remain separate ordered arguments.
See the native signatures and limitations.
The complete typed reachability example
constructs destructured native parameters, optional pop arms, a loop and explicit
user_info/same_operation calls without generating query text:
cargo run -p zirium --locked --example typed_worklistsIt prints the same zero-edge, reachable and disconnected results as the
text program.
evaluate_output_with_budget retains native computational results. Supply a
retained result with ReportExpr::native_input(output) for continued library
use; handles and computational context are checked against the current document.
Use one active budget for evaluation and explicit conversion. Ordinary native
tuples and positional sequences can be converted to JSON. A queue or identity
set anywhere inside a value produces a located conversion refusal.
The shared runtime retains immutable queue/set roots and scan snapshots. Advancing a queue or inserting an identity preserves previously retained values. The host adapter supplies exact document owner/revision and checked object identity, plus complete direct SSA-user adjacency; it runs no second loop or worklist evaluator. Python keeps its existing query builder API.
Match projected strings
Section titled “Match projected strings”Rust ReportExpr and Python ReportExpr provide contains, starts_with and
ends_with. Each method accepts another report expression; Python also accepts
ordinary string arguments. Matching is exact and case-sensitive, empty searches
match, and both operands must evaluate to strings. Unicode text is compared
without normalization. Scans and failure diagnostics use the query budget.
use zirium::query::{ReportExpr as E, report};let query = report(E::string("Ångström").contains(E::string("ström")));from zirium.query import ReportExpr as E, op, ops, report
label = E(ops().filter(op("func.func")).attr("sym_name").one())result = document.query(report(label.starts_with("work")))Select exactly one function before using one(). Use ReportGenerator to
filter several projected names; the string-matching example
shows the equivalent text program and Unicode controls. Domain name extraction
continues to use native projections such as attr("sym_name") and names.
Declaration metadata in the Rust adapter
Section titled “Declaration metadata in the Rust adapter”The adapter uses Zirql’s budgeted declaration copying and structural
native inspection. Copies retain empty sequence element contracts and field
order; Zirium stores the copied declaration in Arc and retains native owner
checks. Shared scalar type-fact admission checks primitive arguments and
predicates. A declared scalar can supply an incomplete actual fact during
preparation; bare unknown facts remain a refusal. Runtime values still pass
the existing native or constructed admission checks.
Count and index arguments use Zirql’s located nonnegative integer conversion, including platform overflow refusal, with Zirium’s argument diagnostics. MLIR kind conversion, native handles, source coordinates and domain admission remain in Zirium. These are Rust adapter APIs, not additional Rust/Python query syntax. See the shared API contract.
Markdown reports
Section titled “Markdown reports”Textual reports evaluated through the Rust library use the same shared Markdown
renderer as the CLI. Collect each QueryOutput::Text after the query succeeds:
use zirium::{dialect::DialectRegistry, query::{Query, QueryOutput}};let query = Query::parse( "match { from subject in (input | filter(op(\"func.func\"))); } | markdown;")?;let mut output = Vec::new();query.evaluate(&mut document, DialectRegistry::baseline(), |_, value| { if let QueryOutput::Text(text) = value { output.extend_from_slice(text.as_bytes()); } Ok(())})?;document is the lowered MLIR document described above. Commit the collected
bytes only after evaluate succeeds, so a later error discards the whole report.
The CLI recipe uses the same capture rows.
Zirql owns table layout, escaping and budgets;
Zirium retains operation descriptors, source output and emitter policies.
Explicit scalar conversion builders
Section titled “Explicit scalar conversion builders”Rust ReportExpr and Python ReportExpr expose parse_int(), parse_float() and
to_string(). Each forwards to the common unary evaluator; the host supplies no
conversion implementation. For example, Python can build a label from an explicit
native count projection:
from zirium.query import ReportExpr as E, ops, report
query = report({ "count": E(ops().count()).to_string(), "quantity": E("002").parse_int(), "price": E("1.25e1").parse_float(),})result = document.query(query)In Rust, the corresponding scalar builders are
ReportExpr::from(ops().count()).to_string(),
ReportExpr::string("002").parse_int() and
ReportExpr::string("1.25e1").parse_float(). See the
shared contract and
textual report recipe.
Collection report builders
Section titled “Collection report builders”Rust ReportExpr exposes filter(pattern, body),
count(), length(), unique(), tally(), head(count), tail(count),
reverse() and check(operand, message). Bounds and optional check operands/messages are report
expressions; use None for omitted Rust check arguments. These methods build the
same prepared value nodes as text and merge parameter/function metadata. Arrays,
sets and dictionaries retain their kind, order and declared empty schemas.
Dictionary predicates receive values, and keys stay attached to retained entries.
Native OpQuery operations retain their existing document ownership and subjects.
unique() retains the first occurrence of each identity. tally() returns a
dictionary of counts whose keys follow first-occurrence order. Constructed values use
structural identity; native leaves retain document ownership and identity. Both
methods retain parameter and function declarations, so their results can be used
with other report methods.
Python mirrors the same operations:
import ziriumfrom zirium.query import ReportExpr as E, ReportPattern as P, report
document = zirium.parse_text('"test.seed"() : () -> ()').lower_strict().documentassert document is not Noneselected = E([3, 1, 3]).filter(P.name("n"), E.name("n") > 1)result = document.query(report(selected.head(2).reverse().check(2, "two rows")))assert result == [3, 3]assert document.query(report(E(["load", "add", "load"]).unique())) == ["load", "add"]assert document.query(report(E(["load", "add", "load"]).tally())) == {"load": 2, "add": 1}assert document.query(report(E({"a": 1, "b": 3}).filter(P.name("v"), E.name("v") > 1))) == {"b": 3}count and length return cardinality, including dictionary entries and present
keyed cells. Other selection operations require explicit positional/entry
conversion of keyed arrays. Tuple and string values are refused. Runtime checks
and native Unknown/completeness rules follow the
shared collection reference.