Language reference
A Zirql expression transforms data into a result. That data may be a string array, a dictionary, or a selection of native objects supplied by an application. The word tutorial starts with an array and follows it through selection, adjacent pairs and grouped occurrences. This reference gives the rules behind those examples.
The application embedding Zirql is its host. Strings, arrays and dictionaries are constructed values, which Zirql can inspect directly. Native values identify objects kept by the host, such as words in a source document or nodes in a syntax tree. A projection reads properties of those objects into constructed values.
The available data and domain operations depend on the application. The word example accepts a single expression; applications can also support complete programs with parameters and named functions. Each chapter identifies where host support is needed. For the version used by these examples, see the setup guide.
Values and expressions
Section titled “Values and expressions”Values include Booleans, signed integers, finite floats, strings, absence, arrays, tuples and dictionaries. Nominal members, typed sets and keyed arrays retain additional type or index information. Native objects keep their host’s identity.
42"apple"["ash", "apple", "birch", "apple"]{"word": "apple", "count": 2}Arrays and tuples use one-based positions. words[1] is "ash" in the word
example; (2, 5).2 selects the second tuple value. Dictionary access accepts constructed keys,
such as row["word"] or counts[{"ash", "apple"}]; row.word selects a string field. Ordinary access fails
when an entry is missing; lookup(row, "word") returns absence for a valid
missing key.
Arithmetic is checked. Integer overflow and division by zero fail, and strings
are not coerced to numbers. ++ concatenates strings. Boolean and and or
short-circuit. /\ and \/ spell the same Boolean operators. A conditional
evaluates only the chosen branch:
if length(words) > 0 { words[1] } else { "no words" }The same conditional can use if length(words) > 0 then words[1] else "no words" endif,
with elseif condition then value for additional branches. All branches still
receive scope and known-type checks. See
values and comprehensions for exact arithmetic, collection,
absence and access rules.
Names may be single-quoted, such as 'row count'. Quotes are omitted from
name identity while diagnostics retain the original bytes; 'if' reads a name.
Strings use double quotes and JSON escapes. Whitespace and #/% line comments
or nonnested /* ... */ comments separate tokens. Integers also accept 0x/0X
hexadecimal and 0o octal forms within signed 64-bit bounds. Hexadecimal floats
are unsupported.
Natural records such as (word: "apple", count: 2) have the same ordered string
fields as the dictionary example above. Indexed values accept a[i, j] as
a[(i, j)]; [| 1, 2 | 3, 4 |] constructs a rank-two indexed matrix, and
[3: 7, 8] gives its cells keys 3 and 4. See keyed arrays
for explicit keys and empty shapes.
row with {limit: row.limit + 4} replaces an existing string field in an
immutable record. Static labels may also be quoted, as in {"limit": 12}.
The base runs once, then replacements run in written order in surrounding
scope. Field order, aliases, native children and declared contracts are retained.
Missing or duplicate target fields fail. General dictionaries, keyed arrays and
field insertion/removal are outside this form. The capacity scenario
runs through the local CSV host and shows both original and updated records.
Save intermediate results
Section titled “Save intermediate results”A let gives a calculated value an immutable name. Later expressions can reuse
it. This complete report filters sample counts, then sorts a dictionary by value:
cargo run --locked --example csv -- 'let samples = [ {"label": "pear", "count": 2}, {"label": "apple", "count": 1}, {"label": "ash", "count": 2}, {"label": "birch", "count": 3}];let selected = [row for row in samples if row.count > 1];sort_by( {row.label: row.count for row in selected}, value, descending=true)' \ examples/csv/people.csv{"birch":3,"pear":2,"ash":2}selected contains pear, ash and birch in their input order. The dictionary uses
their labels as string keys and their counts as values. descending=true puts
the largest count first. Pear and ash both have count 2, so their incoming order
stays pear, then ash. Sorting retains the dictionary kind.
The CSV file supplies the example application’s required input; this calculation
uses only samples. The CSV walkthrough applies the same operations to
file contents. Zirium and
Zincite run the same calculation in
their own programs, alongside operations on MLIR and MiniZinc source.
Bindings are visible after their initializer. They cannot replace an existing name, refer forward or refer to themselves. A comprehension’s iteration names stay inside that expression. The host decides which statement and output forms its frontend accepts; CSV accepts ordered lets and one final expression.
Comprehensions
Section titled “Comprehensions”A comprehension constructs a collection from a source and an optional filter:
[{"word": w} for w in words if starts_with(w, "a")]The head constructs each result. The generator binds w, and the condition
selects which words reach the head. Arrays preserve generation order and
repeated values. Multiple for clauses form a nested traversal; later sources
can use earlier binders.
The MiniZinc spelling uses a generator pipe and where:
[{"word": w} | w in words where starts_with(w, "a")]Both forms use the same patterns, scope and evaluator. The head sees every
binder. Each source sees only earlier generators; its filter also sees the
current generator’s names. Grouped binders such as x, y in [1, 2] form a
Cartesian product, with the rightmost binder varying fastest. Commas separate
MiniZinc generators. That spelling also supports a single-value binding:
[total | x in [1, 2], total = x * 2 where total > 2] returns [4].
The common spelling requires for pattern in source.
Add case before a pattern to skip ordinary pattern mismatches:
[n for case present(n) in [1, null, 2]] and
[n | case present(n) in [1, null, 2]] both return [1, 2].
For grouped binders, the modifier selects whole combinations. case is not
available with pattern = value. Errors and Unknown guards still fail; rejected
candidates still spend work. See the runnable mixed-record example.
Parentheses protect a native pipeline in a head or source. A pipe after the
closing collection starts its suffix, as in [x * 2 | x in [1, 2, 3] where x > 1] | count.
Bare braces construct a set without an outer colon, or a dictionary with one:
{n mod 3 for n in 1..5}{w: length([w]) for w in ["pear", "apple"]}The set keeps 1, 2, 0; the dictionary keeps pear, then apple.
Keys and set elements can be any constructed value, including nested
collections. Duplicate dictionary keys fail before their values run. Empty {}
is a dictionary; set<value>() is an empty general set. Empty scalar sets use
set<int>(), set<bool>(), set<float>() or set<string>(). See values
for structural equality, general dictionary lookup and typed contracts.
Indexed brackets retain integer or nominal row and column keys:
[2: 20, 5: 50] axes([5, 2, 9])[(i, j): j for i in [2, 5] for j in [8, 1]] axes([5, 2, 9], [8, 1, 4])[] axes(set<int>(), [8, 1])The optional axes(...) suffix declares complete ordered key domains, including
keys without cells. Its sources run in surrounding scope before the contents.
Without it, nonempty keys establish their dimensions. Empty [] is positional;
a keyed empty result needs established domains. See keyed arrays.
Comprehensions are eager. A reducer such as length or sum receives the
completed array, and rejected candidates still cost work. See
positional comprehensions and
keyed arrays for construction and index rules.
Built-in operations
Section titled “Built-in operations”The library reference indexes every supported shared function, pipeline stage and operator. Browse its purpose groups for complete input, result, empty-input and error contracts. The table below is a quick selection of common constructed operations.
These operations work on constructed values. Native stages can have similar names but operate on a host’s native selections.
| Operation | Meaning |
|---|---|
contains(s, part), starts_with(s, prefix), ends_with(s, suffix) |
Exact, case-sensitive string matching |
length(a), sum(a) |
Count present array entries or sum integer entries |
forall(a), exists(a) |
Reduce an array of known Booleans |
sort(a), sort_by(d, value), sort_by(rows, "field") |
Stable same-kind array, set or dictionary ordering, optionally descending=true |
min(a), max(a), min_by(rows, "field"), max_by(rows, "field") |
Select an extremum, or absence on empty input |
group_by(rows, "field") |
First-seen groups, each with key and items |
align(left, right, by="field") |
Full alignment by unique present structural keys; key, optional before and optional after, in left then right-only order |
lookup(a, key), has_key(a, key) |
Optional access and membership |
occurs(v), absent(v) |
Test presence or absence |
v default fallback |
Evaluate the fallback only when v is absent |
one(a) |
Require exactly one element |
require(condition, value, "message") |
Return the value on true; otherwise fail with the message |
index_set(a) |
Return typed keys of a positional or rank-one keyed array |
entries(a), zip_exact(a, b) |
Inspect keyed cells or pair arrays with identical axes and present keys |
The value reference defines accepted types, empty inputs and error
cases. For example, length accepts arrays, not strings, and sum uses checked
integer addition. forall and exists inspect an eager Boolean array; they do
not turn its construction into a lazy traversal.
Constructed pipelines
Section titled “Constructed pipelines”The common reader accepts constructed filtering, mapping and collection stages:
{"beginning_with_a": filter(starts_with(_, "a")) | count, "total": count}["ash", "apple", "birch", "apple"] | filter(starts_with(_, "a")) | sort | unique | count[w for w in words if starts_with(w, "a")] | sort | unique[w for w in words if starts_with(w, "a")] | tallyThe first two return {"beginning_with_a": 3, "total": 4} and 2 with the
word host. Bare filter/count use the same constructed input independently in
each dictionary field; an explicit source supplies its own stages. The filter
accepts pattern => Boolean_expression or an expression using _.
The subject is one positional element; named and unnamed forms agree with
comprehension selection. For example, people | filter(_.active == "yes") and
people | filter(p => p.active == "yes") retain the same rows. Nested predicates
rebind _; explicit outer names retain lexical captures. Destructured arguments
still give _ the whole subject. _ is unavailable outside a predicate argument
and keeps its wildcard meaning in patterns. Empty and no-match inputs count as
zero. Count returns a scalar, so a
later collection stage is invalid. Missing input and incompatible types receive
located errors. The last two return ["apple", "ash"] and
{"ash": 1, "apple": 2}. Pipes apply
left-to-right after ordinary operators, defaults, comprehensions and
conditionals. Tally is terminal. Known constructed bindings can pipe directly,
such as words | unique. Grouping also works, including for a user call such
as (make_words()) | tally. Fields compose with the same stages:
[outer.items | filter(_ > 1) for outer in [{"items": [1, 2, 3]}]]This returns [[2,3]]; grouping outer.items gives the same result. A nested
filter gives _ its current element and keeps named outer arguments available.
Native fields retain their host’s stage meanings and native values. See collection pipelines
for supported scalar kinds, empty results and grouping in larger expressions.
Map and flat_map
Section titled “Map and flat_map”map(pattern => expression) transforms each positional item once. The unnamed
form accepts an expression using _ in the same scope. For example,
[2, 3] | map(_ + 1) returns [3, 4]. A body can return a scalar, record,
collection or supported native value.
flat_map requires each body result to be a positional sequence and concatenates
one level in source order: [2, 3] | flat_map([_, _ + 10]) returns
[2, 12, 3, 13]. Neither stage implicitly iterates sets, dictionaries, keyed
arrays, tuples or opaque native selections. Native sequences need explicit
positional host views. Both stages return eager positional sequences.
Nested arguments rebind _; named outer arguments remain lexical. Patterns use
the existing strict mismatch and no-shadowing rules. Every written body receives
scope, type and purity checks before iteration, including on empty input. Source
traversal, body construction and appended results spend the same request budget;
stream limits also apply to the expanded output. Failure returns no partial
result. Bare map(...) and flat_map(...) use the host’s constructed input.
The capacity report runs with the word example; the CSV report uses named tables. These are Zirql entry points. Consumer frontend adoption is described separately in embedding programs.
Pipelines and native data
Section titled “Pipelines and native data”For native data, a pipeline applies stages in written order. A host exposing borrowed words can support:
filter(prefix("a")) | head(1) | countHere prefix is a host predicate that tests a word’s spelling. It is separate
from the built-in starts_with function used with constructed strings. Other
hosts can define predicates over syntax nodes or document handles. Zirql
supplies the shared sequencing and Boolean composition.
Pipeline semantics cover filtering, ordering, identity,
terminal results and the small parse_pipeline reader. The broader native
representation also supports host stages and set operations. A host can reuse
the common readers or lower an existing parser to that representation.
Sets and dictionaries preserve insertion order and retain their kind through
explicit sorting. sort(d) orders dictionary keys; sort_by(d, value) orders
values and retains current-order ties. Bare braces accept sets and dictionaries,
including computed collection keys. See collection order
for syntax and examples. Both native hosts retain dictionary order through
saved results and JSON rendering.
Use project(subject, {fields}) to turn native subjects into report dictionaries.
Explicit projections produce constructed values; native captures and paths can
retain the original objects. See native programs for
projection, match { from ...; bind ...; where ...; } and bounded paths.
Bindings, parameters and functions
Section titled “Bindings, parameters and functions”Programs execute statements in order and save values in immutable bindings. A
name must be available before use. Required external parameters have int,
string or bool types and are checked before domain execution.
The common read_program reader assembles pattern let, param name: type,
report and ordinary evaluation statements. Bindings and parameters require
;; native hosts accept an optional final expression semicolon. Function
declarations have a balanced body and no separator. A host supplies native
expressions/types, domain statements and output policy. A plain expression
needs no declarations. CSV uses the same reader’s let-plus-expression subset,
with no final semicolon, parameters, functions or report.
Parameters and bindings are sequential: a later declaration cannot satisfy an earlier use, and repeated names fail at their written definition. All function names are available before bodies and statements are read. A function receives its arguments explicitly and cannot capture program bindings, parameters or current input. Strings, comments and nested bodies do not introduce top-level functions. See text program embedding for the callable reader boundary.
Named pure functions have typed parameters and a result type:
fn twice(n: int) -> int { n * 2 }Functions can call themselves or each other recursively. They can inspect native data through registered pure operations, but cannot emit output or edit the source. See function declarations for parameter types, scope and recursion limits.
Patterns and control flow
Section titled “Patterns and control flow”Value matching selects the first matching arm and requires a final unguarded wildcard:
match (length(words)) { 0 => "empty", 1 => "one word", _ => "several words"}Patterns destructure tuples, positional lists and records, bind names and test
optional presence. [a, b] requires exactly two list elements; [a, b, ..]
discards a remainder after at least two. [] requires empty and [..] accepts
any positional list. Tuples keep exact tuple arity; keyed arrays and sets do not
match list patterns. Lets, typed parameters, predicate arguments and comprehension
binders use these same patterns. Ordinary bindings report located mismatch
errors. A case generator skips the candidate; value-match arms try the next
pattern. A native capture match enumerates correlated domain
objects; a value match selects one result. Their forms and results are different.
The functions and control flow chapter describes matching, guarded arms, local bindings, loops and named reducers. The common reader supports these forms; each host connects them to its program and native-type handling.
Errors and limits
Section titled “Errors and limits”A missing field, incompatible type, failed requirement or exhausted allowance is an error with a location. An absent value is ordinary data. An unknown native predicate is a third truth state, used when the host lacks a fact; negation preserves that uncertainty.
The application chooses finite limits for parsing and execution. Nested queries, comprehensions and host callbacks share the active budget. See diagnostics and limits for the contract.
Explicit scalar conversion calls
Section titled “Explicit scalar conversion calls”parse_int(text), parse_float(text) and to_string(value) are pure common
one-argument calls, usable in reports and typed functions. Their fixed names cannot
be redeclared as functions. Scalars and strings
defines the exact decimal grammar, finite numeric boundaries and scalar formatting.
Known incompatible operands fail preflight; malformed text in an unchosen branch
is not parsed.
String formatting and interpolation
Section titled “String formatting and interpolation”format(value, spec) formats a Boolean, integer, finite float or string. An empty
specification matches to_string; [align][0][width][.precision][type] selects
alignment, numeric zero padding, Unicode-scalar width and explicit numeric
formatting. See scalar formats for the supported
types and strict operand rules.
Use lowercase f"..." for interpolation: f"count={2:03d}" returns "count=002".
Each {expression} evaluates once, from left to right, in the current scope;
:spec supplies a literal format specification. Nested expressions, collections,
pipelines, strings and f-strings use the shared reader. {{ and }} produce
literal braces, and JSON escapes apply to literal text. Escapes do not create
fields. Empty fields, bare closing braces and malformed specifications fail
while reading. Dynamic nested specifications, conversion flags and debug fields
are unsupported. Plain strings keep their literal meaning.
Known types receive preflight checks even in skipped branches and unused functions. Dynamic specification contents and value-dependent formatting remain lazy. Native values require explicit scalar projection. Parsing and output expansion share the request budget, and errors retain original byte ranges.