Inspect MiniZinc source
Use zincite query when you have a specific question about a model or its
data. You can select constraints, count written calls, follow declaration
uses or compute a report from literal data. Queries do not run a solver.
The staffing walkthrough starts with a practical question: which day has too few staff, even though the total availability looks sufficient? The examples below develop source inspection and data reports in more detail.
This guide develops MiniZinc source and data recipes. The Zirql library reference owns shared function and operator contracts. The expression contract describes Zincite syntax, native projections and the smaller language allowed in data edits.
Zirql evaluates shared expressions, bindings and collection operations, including native filtering, matching, ordering, dictionaries, existence, fixed points and paths. Zincite supplies MiniZinc input, domain operations and predicate facts, native identity, source ownership and complete-observation rules. Zirql renders the ordinary report containers and constructed data. The Zirql language reference offers further examples and background on the evaluator shared with other tools. Its shared native reference describes common composition; the recipes below supply Zincite’s actual native operations and source/model policies.
Saved repository examples use the v0.5.0 checkout. Run their repository-relative paths from the checkout root. Examples that give file contents can be run in a new directory with the installed commands.
Program syntax and output
Section titled “Program syntax and output”--program and --program-file use Zirql’s common declaration and statement
reader. param name: int|string|bool; declares caller data; let pattern = expression; saves a value; report expression; requests JSON. Ordinary query
statements choose native source output or report output from their result kind.
A final evaluation may omit its semicolon. Functions use fn name(...) -> Type { ... } without a following separator. A single query needs no declarations.
Function names are available throughout the program. Parameters and bindings become available at their written positions, and a repeated name is an error. Functions receive arguments explicitly; they cannot capture program parameters, bindings or the current input. The reader ignores function-like text inside strings, comments and nested bodies.
Zincite retains MiniZinc quoted names, source-copy operations and the smaller
Data/edit language described in the expression contract.
create, replace, append and scale_enum form an atomic batch ending with
emit; (emit_document; is its alias). No statement follows that terminator.
Bindings cannot edit or emit. Parsing builds syntax only; preparation and
execution validate native facts and the complete candidate before output.
Zincite output stages remain terminal and later errors leave stdout empty.
Typed string results work directly as tally keys:
fn echo(s:string)->string {s}report [echo("ash"),echo("ash")] | tally;The result object is {"ash":{"kind":"integer","value":2}}, matching a
literal string tally. Zincite retains its tagged integer output.
Run the existing function recipe from the Zincite 0.5.0 checkout:
cargo run --locked --bin zincite -- query --program-file \ crates/zincite-query/tests/fixtures/recursive-functions.zq \ crates/zincite-query/tests/fixtures/recursive-functions.mznSelect items and calls
Section titled “Select items and calls”Save this small model as model.mzn. It shares a capacity across three
allocations:
int: capacity = 20;array[1 .. 3] of var 0 .. capacity: allocated;constraint sum(allocated) <= capacity;solve satisfy;Count its constraints, print them, or count the calls written inside them:
zincite query 'filter(kind("constraint")) | count' model.mznzincite query 'filter(kind("constraint"))' model.mznzincite query 'filter(kind("constraint")) | expressions | call_names | tally' model.mznThe first command prints 1. The second prints the constraint’s source, and
the third prints {"sum":1}. Stages are separated by |; each works on the
previous stage’s result. The standalone zincite-query command accepts the
same options. Syntax inspection needs no standard library.
The feature-extraction cookbook develops these counts and histograms and adds declaration types and literal shapes. The query reference defines stages, selection order and result formats.
Keep a suffix, reverse it and check its size
Section titled “Keep a suffix, reverse it and check its size”Native selections support tail(n), reverse and pass-through cardinality checks:
let selected = items | filter(kind("declaration"));let n = selected | count;let message = "selection size changed";selected | tail(n) | reverse | check(n, message) | namesRun this with --program against model.mzn above. It prints allocated,
then capacity, one name per line.
tail keeps a suffix in its current order; reverse then reverses those rows,
including duplicate occurrences. Zero keeps no rows and an oversized bound keeps
all rows. Saved selected retains its original rows for later use. These stages
keep native identities and original source ranges. Reordered item output carries
its attached comments, with unsafe formatter directive pairs still refused.
Bare check requires a nonempty stream. check(n) requires exactly n rows;
check(message) and check(n, message) add a failure message. Success passes the
stream through unchanged; failure reports the expected and actual counts. An
empty stream passes check(0) and fails bare check. check() is invalid.
Program counts and messages can refer to parameters, bindings and local scopes.
Positional queries use literal counts and double-quoted messages:
zincite query \ 'filter(kind("declaration")) | tail(2) | reverse | check(2, "need two declarations") | names' \ model.mznA check counts returned rows, including when a semantic result has limitations.
It does not prove complete domain knowledge or count the cells inside a literal
array. Use elements to select those cells. The Boolean require expression
checks a condition separately. These controls apply to native streams; they do
not add constructed-array operations or new data-edit stages.
Reuse results in a report
Section titled “Reuse results in a report”Save this as report.zq:
let selected = items | filter(kind("constraint"));let n = selected | count;let histogram = selected | expressions | call_names | tally;(count: n, histogram: histogram, selected: selected)Run zincite query -f report.zq model.mzn or
zincite-query --program-file report.zq model.mzn. The JSON output has
complete, limitations and result fields. Here, result contains the count,
histogram and selection. The selected nodes keep their file, byte range and exact source text.
No model facts are needed for this syntax report. Use declarations
in the input binding to select semantic declaration identities instead.
let evaluates its right-hand side once and gives the result an immutable
name. selected | count and selected | expressions reuse that selection.
A pipeline beginning with items starts from the original input again.
An expression outside a binding emits its result. Here, the final record
produces JSON automatically. Ending with just selected would print the selected
source; report selected would request its JSON representation instead.
Earlier output expressions also emit, so use let for intermediate values you
want to keep out of the output.
Bindings and earlier statements need semicolons. The final expression may end without one. Statements run in written order, and a later failure leaves stdout empty. Query programs describes output formats, the library result and aggregate limits.
The following examples build records and arrays from literal data. Their JSON uses explicit type tags and array keys, so the tables in this guide show the values rather than the full serialization. The reference calls pipeline selections, counts and histograms native results to distinguish them from these calculated values. The result schema describes how to read those reports from another program.
All examples here are read-only. To change a data file, use the data-edit guide; editing supports a smaller set of expressions.
Calculate with shared values
Section titled “Calculate with shared values”A report can calculate entirely from values in the query. Save this as
counts.zq:
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)zincite query --program-file counts.zq model.mzn{"complete":true,"limitations":[],"result":{"birch":{"kind":"integer","value":3},"pear":{"kind":"integer","value":2},"ash":{"kind":"integer","value":2}}}[row for row in samples if row.count > 1] visits each array element in order.
row names that element, .count reads its record field, and if keeps only
elements for which the comparison is true. Omitting if keeps every element;
the expression before for chooses what to put in the result array.
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.
Zincite accepts both [row for row in samples if row.count > 1] and
[row | row in samples where row.count > 1]. Both use Zirql’s reader and
evaluator, with the same scope, order and limits. Dictionaries keep insertion
order and reject duplicate keys; sets keep the first occurrence of each value.
Sorting is explicit. The dictionary examples
show these rules locally; Zirql’s
collection-order guide
provides further detail. Zincite wraps calculated integers in the kind/value
objects shown above and adds the report’s completeness fields.
This program does not inspect model.mzn. To supply data from MiniZinc source,
select native items and project their names or literal values, as in the
next examples. Model evaluation, declarations and source edits retain their
MiniZinc-specific rules.
Use --program for an inline complete program or --program-file for a file.
The positional query argument reads the smaller native pipeline syntax and does
not accept let declarations.
Select values with a predicate
Section titled “Select values with a predicate”filter(row => row.count > 1) names the current element. The equivalent
filter(_.count > 1) uses its implicit argument. Both keep the same rows as
[row for row in samples if row.count > 1], in the same order. Bodies can use
visible immutable bindings and pure query functions.
Tuple and list patterns work in bindings, function parameters, predicates, comprehensions and value-match arms:
let [limit, extra, ..] = [2, 1, 99];fn above((value, bound): tuple<int,int>) -> bool { value > bound }let pairs = [(1, limit), (4, limit), (3, limit + extra)];[pair for pair in pairs if above(pair)]A tuple pattern requires exactly its written number of positions. [a, b]
requires a two-element positional list; [a, b, ..] requires at least two and
discards the rest. Sets and keyed arrays do not become lists for matching. A
binding, parameter, predicate or generator mismatch is a located error. A value
match tries its next arm instead. _ remains a wildcard in patterns; inside
a predicate body it reads the whole current subject, even when a pattern names
its parts. Nested predicates bind _ anew and retain explicitly named outer
captures.
Native filter, paths(..., to=...) and native match guards accept the same
named and implicit bodies. Native values keep their existing projections:
items | filter(item => length(item | names) > 0) tests each source item.
A path target receives one candidate declaration. A native match guard receives
the original query input, with earlier captures available under their own names.
These rules do not add fields to MiniZinc nodes or declarations.
Continue a calculated array
Section titled “Continue a calculated array”A saved selection can feed several calculations before the report is rendered. For a data file, this program sorts the assignment names and retains the selection alongside its count:
let selected = items | filter(kind("assignment"));let labels = selected | names;let n = selected | count;let rows = [label | label in labels];let ordered = rows | sort | unique;(names: ordered, count: n, selected: selected)Run zincite query -f names.zq data.dzn, or pass the same source with
--program. The comprehension uses MiniZinc’s generator pipe inside brackets.
The outer pipes continue the completed array through Zirql’s shared collection
operations. sort uses structural value order; unique keeps the first structurally
equal value, including mixed scalars and record rows. For string frequencies, replace
the last binding with
let ordered = rows | tally;. Its result is a dictionary with string keys in first-seen order
and integer counts.
To count only names beginning with a, continue the calculated array:
let selected = items | filter(kind("assignment"));let labels = selected | names;let rows = [label | label in labels];let matching = rows | filter(starts_with("a"));matching | countThe filter uses exact, case-sensitive prefixes. count counts the retained
strings, including duplicates. The same suffixes work on a saved function
result: bind let labels = make_labels();, then continue labels | sort.
A saved calculated value can feed a pipeline directly: rows | sort | unique.
Group a function call when continuing its result, as in (make_rows()) | tally.
Native pipelines such as selected | names keep their domain stages.
names, count and values provide explicit calculation inputs from native
results; no text or JSON conversion is involved. The same expressions work
with an unrelated function declaration and inside supported pure functions.
These suffixes are read-only report operations. Keyed arrays need an explicit
positional conversion, such as [e.value | e in entries(a)] | sort. See the
constructed pipeline contract
for empty inputs, supported types and failures.
Read a named assignment
Section titled “Read a named assignment”Create data.dzn with demand and capacity arrays:
demand = [2, 3, 4];capacity = [6, 8, 5];Save this as totals.zq:
let demand = items | filter(name("demand")) | values | one;let total_demand = sum(demand);let capacity = items | filter(name("capacity")) | values | one;(total_demand: total_demand, total_capacity: sum(capacity))zincite query -f totals.zq data.dznThe result contains total_demand = 9 and total_capacity = 19. The input
pipeline reads the demand in four steps:
| Stage | Result |
|---|---|
items |
The two assignments in data.dzn. |
filter(name("demand")) |
The assignment named demand. |
values |
A sequence containing its literal array [2, 3, 4]. |
one |
The array itself, extracted from that sequence. |
one checks the number of projected values, not the number of array elements.
Zero or multiple matches fail. values reads written literals; it cannot
evaluate a MiniZinc right-hand side such as [n * 2 | n in 1..3].
Input reads and calculations use the same let syntax and can appear in the
order you need them. Here, total_demand is calculated before capacity is
read. The quoted name chooses an assignment in the file; the binding name is
how the query refers to its value. For example, let required = items | filter(name("demand")) | values | one; would read the same demand array.
Compare demand and capacity
Section titled “Compare demand and capacity”Using the same data.dzn, suppose demand may double. We want to compare the new
demand with capacity at each position, as well as in total. Save the following
as capacity.zq. The factor is a local literal binding.
let demand = items | filter(name("demand")) | values | one;let capacity = items | filter(name("capacity")) | values | one;let factor = 2;let positive_factor = require(factor > 0, factor, "factor must be positive");let aligned_capacity = require( index_set(demand) = index_set(capacity), capacity, "demand and capacity must have the same index set");let scaled = require( forall([d >= 0 | d in demand]) /\ forall([c >= 0 | c in aligned_capacity]), [positive_factor * d | d in demand], "demand and capacity must be nonnegative");let rows = [ (index: i, demand: scaled[i], capacity: aligned_capacity[i], headroom: aligned_capacity[i] - scaled[i]) | i in index_set(demand)];let total_demand = sum(scaled);let total_capacity = sum(aligned_capacity);( rows: rows, total_demand: total_demand, total_capacity: total_capacity, totals_fit: total_demand <= total_capacity, cells_fit: forall([r.demand <= r.capacity | r in rows]), violations: [r | r in rows where r.demand > r.capacity])Run zincite query --program-file capacity.zq data.dzn or pass the same text
through --program. The program returns these rows:
| Index | Scaled demand | Capacity | Headroom |
|---|---|---|---|
| 1 | 4 | 6 | 2 |
| 2 | 6 | 8 | 2 |
| 3 | 8 | 5 | -3 |
The totals are 18 and 19, so totals_fit is true. cells_fit is false and
violations contains only row 3. This negative finding is successful report
data: complete is true, limitations is empty and the command exits 0.
The require calls check the assumptions before calculating the rows: the
factor must be positive, the arrays must have the same indices, and values
must be nonnegative. A failed requirement stops the report with its message.
sum adds the scaled demand; forall checks that every row fits. These calls
evaluate query values, not the MiniZinc model. The
expression reference
covers the supported operations and their error cases.
Use a query as a checker
Section titled “Use a query as a checker”To reject insufficient capacity, put the condition inside require. Save this
program as capacity-check.zq:
let demand = items | filter(name("demand")) | values | one;let capacity = items | filter(name("capacity")) | values | one;let aligned_capacity = require( index_set(demand) = index_set(capacity), capacity, "demand and capacity must have the same index set");require( forall([demand[i] >= 0 /\ aligned_capacity[i] >= demand[i] | i in index_set(demand)]), true, "each capacity must cover its nonnegative demand")For demand = [2, 3, 4]; capacity = [6, 8, 5];, every cell fits and
the program returns true. Changing the final capacity to 3 fails with
each capacity must cover its nonnegative demand and exit status 2.
Mismatched index sets fail before cell access.
Both query commands accept the file or its text:
zincite query --program-file capacity-check.zq data.dznzincite-query --program-file capacity-check.zq data.dznzincite query --program "$(cat capacity-check.zq)" data.dznzincite-query --program "$(cat capacity-check.zq)" data.dznUse the exit status in Bash. Redirect stdout to suppress the successful report; diagnostics still go to stderr, and a failed requirement selects the else branch.
if zincite query --program-file capacity-check.zq data.dzn >/dev/null; then echo "capacity check passed"else echo "capacity check failed" >&2 exit 1fiA plain false expression is a successful report value. A checker must use
require to turn a failed condition into an error. Unknown conditions and
evaluation errors also fail; they cannot satisfy a requirement. As with other
query failures, a later failed requirement leaves stdout empty even when an
earlier statement requested output.
Library callers use the same program:
use zincite_query::{Input, Program, ProgramLimits};use zincite_syntax::FileMode;
let input = Input::parse( b"demand=[2,3,4]; capacity=[6,8,5];".to_vec(), FileMode::Data,)?;let limits = ProgramLimits::default();let checker = Program::parse(include_str!("capacity-check.zq"), limits)?;let checked = checker.evaluate(&input, limits)?;assert!(checked.is_complete());Construct indexed arrays
Section titled “Construct indexed arrays”Use integer or resolved enum keys when a cell’s address should survive a filter.
A tuple supplies row and column keys. Save this as indexed-arrays.zq, or use
the saved example:
let sparse = [(5,8):58, (2,1):21, (2,8):28];let generated = [(i,j):i*10+j | i in [5,2], j in [8,1] where i=2 \/ j=8];let empty = [(i,j):1 | i in 1..0, j in {8,1}];let unused = [i:i | i in [5,2,9] where false];(sparse:sparse, same:sparse=generated, cells:entries(sparse), empty:empty, unused:unused)Run it against the model.mzn above:
zincite query --program-file indexed-arrays.zq model.mznzincite-query --program-file indexed-arrays.zq model.mznBoth commands return the same complete tagged report. The same field is true.
The row keys are 2, 5 and the column keys are 1, 8; these ordered domains
are the array’s axes. Stored cells follow row-major order:
| Row | Column | Value |
|---|---|---|
| 2 | 1 | 21 |
| 2 | 8 | 28 |
| 5 | 8 | 58 |
There is no cell at (5, 1). Reports retain this sparse shape, while constructed
data edits still require contiguous keys and rectangular coverage. Numeric keys
are inferred in numeric order; resolved enum keys use declaration order and keep
their model identity. entries returns only present cells. index_set returns
the full keys of a rank-one array, including declared keys without a cell.
The empty result retains two dimensions: a typed empty integer row domain and
column keys 8, 1. The unused result has no cells but keeps keys 5, 2, 9.
For these natural MiniZinc comprehensions, simple generator key names provide
a lazy zero-cell fallback from the surrounding source values. It runs only
when no cells were produced. Model-backed typed empty nominal sources retain
their enum identity too. Rust callers can supply eager declared domains through
the typed value API
when nonempty results must also retain unused keys or a chosen domain order.
Compare keyed matrix cells
Section titled “Compare keyed matrix cells”To compare two matrices, match cells by their indices before subtracting
the values. Save this as matrix.dzn:
before = [|3, 4 | 5, 6|];after = [|4, 6 | 8, 10|];Save the query as matrix-delta.zq:
let a = items | filter(name("before")) | values | one;let b = items | filter(name("after")) | values | one;let pairs = zip_exact(a, b);[e.key: e.value.2 - e.value.1 | e in entries(pairs)]zincite query --program-file matrix-delta.zq matrix.dznzip_exact requires both arrays to have the same ordered axes and stored keys.
It pairs the before and after values at each cell. entries exposes each key
and pair, so the comprehension can subtract the first value from the second
and keep the original key.
The result has axes 1..2 by 1..2 and these values:
| Row | Column 1 | Column 2 |
|---|---|---|
| 1 | 1 | 2 |
| 2 | 3 | 4 |
Different axes or missing keys on only one side produce an error rather than
pairing unrelated cells. For empty arrays, report zip_exact(a, b) directly
if you need to retain the declared axes: this comprehension’s e.key head
cannot infer them from no entries. These helpers are available in reports,
not data edits.
Group machine demands
Section titled “Group machine demands”A total for each machine is more useful than a count of all jobs. Save these
jobs as jobs.dzn:
jobs = [(machine: "b", demand: 3), (machine: "a", demand: 5), (machine: "b", demand: 7), (machine: "b", demand: 3)];Save this as machine-totals.zq:
let rows = items | filter(name("jobs")) | values | one;[ (machine: g.key, count: length(g.items), total: sum([r.demand | r in g.items])) | g in group_by(rows, "machine")]zincite query --program-file machine-totals.zq jobs.dzngroup_by gives each group a key and its original items. The query counts
the jobs and sums their demands:
| Machine | Jobs | Total demand |
|---|---|---|
| b | 3 | 13 |
| a | 1 | 5 |
Groups follow first occurrence, so b comes before a. Both jobs with demand
3 remain in the b group. Grouping preserves duplicates; it does not assume
that equal records describe the same job.
The input here is a positional array. To group a keyed array, first make its
values positional with [r | r in keyed_rows]. Group keys must have compatible
types, and every row must contain the selected field.
Inspect semantic facts
Section titled “Inspect semantic facts”The previous examples inspect syntax or literal values. Finding the uses of a declaration needs more context: Zincite must resolve names in the model and its included files.
The semantic examples below use an installed MiniZinc standard library. Set
MZN_STDLIB_DIR to the directory containing std/stdlib.mzn. If MiniZinc and
Python 3 are on your path, you can read it from the installation:
export MZN_STDLIB_DIR=$( minizinc --config-dirs | python3 -c 'import json, sys; print(json.load(sys.stdin)["mznStdlibDir"])')Alternatively, pass --stdlib-dir DIR on each command. Use -I DIR for project
include directories.
Return to model.mzn, the capacity-and-allocations model at the start of this
guide. Find the written uses of capacity:
zincite query 'filter(name("capacity")) | declarations | uses | json' model.mznfilter selects its declaration item, declarations identifies the declared
value, and uses finds references to that value. The result includes its use
in the array domain and in the constraint, with their file locations.
Semantic JSON includes complete, limitations and result. Without a
configured library, even a small model may produce available results with
complete: false and exit 1. Read the limitations before treating the result
as a complete inventory. Errors such as a missing include exit 2.
For stdin, semantic inspection also needs --model pointing to a file with
exactly the same root bytes. --stdin-filepath supplies a name and settings
location, but does not load that model. The semantic reference
describes selection and context in detail.
Correlate declarations with their uses
Section titled “Correlate declarations with their uses”To report a name and a use count for each declaration in model.mzn, save
this as use-counts.zq:
subtree | declarations | unique | project(d, { "name": d | names | one, "uses": d | uses | count})zincite query --program-file use-counts.zq model.mznproject evaluates both fields for the same declaration d. names reads its
name, and one requires exactly one value. uses finds that declaration’s
references. unique removes repeated visits to the same declaration; two
local declarations with the same name would remain distinct.
Zirql prepares the fields before visiting declarations, so an empty selection still receives scope, type and purity checks. Fields run in written order and the resulting array retains its record schema, including when it is empty. Declaration and use selections keep their native identities and source coordinates; only the projected names and counts enter these report fields. The core projection contract describes this boundary for embedding callers.
For individual uses, save this as uses.zq:
let matches = match { from d in (subtree | declarations | unique); from u in (d | uses);};matches | project(m, { "name": m.d | names | one, "text": m.u | text | one})zincite query --program-file uses.zq model.mznThe first from chooses a declaration. The second visits its uses. Each result
row keeps both objects, so the report can read the declaration’s name and the
use’s exact source text. In this model, capacity has two uses and allocated
has one. An unused declaration contributes no row to this second report.
These are source relationships. The query does not count how often an expression runs or how many variables MiniZinc creates when flattening. See projections and captures for additional clauses and the library result types.
Explain declaration connections
Section titled “Explain declaration connections”A path query can show how one declaration refers to another through
intermediate declarations. Save this as paths.mzn:
int: base_capacity = 10;int: usable_capacity = base_capacity;int: daily_limit = usable_capacity;solve satisfy;Save the query as paths.zq:
subtree | declarations | filter(name("daily_limit"))| paths(referenced_declarations, to=name("base_capacity"), max_depth=2)| jsonzincite query --program-file paths.zq paths.mznThe result contains the path daily_limit, usable_capacity, base_capacity,
with the original declarations and locations. The depth counts edges: a limit
of two reaches base_capacity, while a limit of one does not.
The query returns one shortest path per source/target pair within the bound. An empty result means no path was established within that depth, not that the declarations are unrelated at every depth. Unresolved references remain limitations. These paths follow written references; they do not describe compiler-generated dataflow. See bounded paths for ordering and result fields.
Filter connections and accumulate declarations
Section titled “Filter connections and accumulate declarations”where_exists(query) keeps each original subject when its singleton-relative
query returns a nonempty native selection. It preserves the incoming order and
duplicates. For this model, saved as connections.mzn:
int: A = if true then B else C endif;int: B = D;int: C = B;int: D = A;int: E = 0;solve satisfy;This query keeps A and C, which refer directly to B:
zincite query --program \ 'declarations | where_exists(referenced_declarations | filter(name("B"))) | names' \ connections.mznThe relation can read saved bindings and function parameters. A complete empty relation rejects the subject. An unresolved endpoint or a final unknown filter condition refuses the nested observation at its responsible source location, including when an earlier query already recorded the limitation. Saving a partial result does not make it complete. Other report limitations remain attached to successful complete observations.
fixpoint(query) repeatedly replaces the current selection. Accumulation uses
an explicit union:
zincite query --program 'let seed = declarations | filter(name("A"));seed | fixpoint(input union referenced_declarations) | names' \ connections.mznThis returns A, B, C, D in that order. The body must keep the exact native
kind and owner. Equality compares identities in order, including duplicates.
An unchanged or stable empty selection succeeds; an alternating selection such
as declarations | fixpoint(reverse) fails as a cycle. The runner retains one
checkpoint and uses the continuing request budget.
Both bodies are pure report queries. Unsupported result kinds and effects fail
during preparation, even on empty input. Filtering a single Node or
Declaration returns the corresponding selection, which can be empty. Fixed
points preserve the exact singleton contract. These operators do not extend the
data-edit expression allowlist.
Read optional fields and sparse cells
Section titled “Read optional fields and sparse cells”Reports can distinguish a stored absent value (<>) from a field or cell
that was not stored at all. This is useful when inspecting incomplete data.
Save the declarations as optional-report.mzn:
array[1 .. 2, 1 .. 2] of opt int: cells;record(int: id, opt int: missing, opt int: stored): info;solve satisfy;Save this partial data as optional-report.dzn:
cells = [(1, 1): 10, (1, 2): <>, (2, 1): 30];info = (id: 4, stored: <>);The declarations tell the query which fields and axes are expected. The data
omits info.missing and cell (2, 2); it is an inspection example, not a
complete instance to pass to a solver. Save this as optional-report.zq:
let cells = items | filter(name("cells")) | values | one;let info = items | filter(name("info")) | values | one;( id: lookup(info, "id") default 0, stored_field: has_key(info, "stored"), missing_field: has_key(info, "missing"), stored_cell: has_key(cells, (1, 2)), missing_cell: has_key(cells, (2, 2)), fallback: lookup(cells, (2, 2)) default 8)zincite query --model optional-report.mzn -f optional-report.zq optional-report.dznThe report gives id = 4, stored_field = true, missing_field = false,
stored_cell = true, missing_cell = false and fallback = 8.
has_key checks whether storage exists, even when its value is <>.
lookup returns absence for a compatible missing key, and default supplies
a value only when its left operand is absent.
Strict access such as cells[2, 2] would fail. A label outside the declared
record schema also fails. A default does not catch type errors, division by
zero or resource limits. These operations are report-only; data edits require
nonoptional targets and complete rectangular arrays.
Sort rows and select extrema
Section titled “Sort rows and select extrema”Continue with jobs.dzn from the machine-grouping example. To put the busiest
machine first, save this as rank-machines.zq:
let jobs = items | filter(name("jobs")) | values | one;let totals = [ (machine: g.key, demand: sum([job.demand | job in g.items])) | g in group_by(jobs, "machine")];( ranked: sort_by(totals, "demand", descending=true), busiest: max_by(totals, "demand"))zincite query --program-file rank-machines.zq jobs.dznranked contains machine b with demand 13, followed by a with demand 5.
busiest is the b record. Sorting preserves input order for equal keys;
min_by and max_by choose the first record on a tie.
See the shared ordering and extrema reference
for supported collections, key order, missing-key failures and empty results.
Use an explicit lookup/default field when a missing score should become zero.
Sorting and extrema are available in reports, not data edits.
Order native selections
Section titled “Order native selections”Keep the selected source items while ordering them by a scalar projection. Save
these declarations as ordering.mzn:
int: A = 1;int: B = 1;int: C = 2;zincite query --program \ 'items | sort_by(expressions | tail(1) | values) | names' \ ordering.mznThe names are A, B, C; A stays before B because their keys tie.
Replace sort_by with min_by to retain A, or min_all_by to retain both
A and B. max_by and max_all_by retain C. The selected nodes keep their
original file and byte ranges. A selector may return an ordinary compound,
Boolean or absent key. Native scalar projections must return exactly one value;
opaque source nodes need an explicit projection. Nominal keys use coherent
declaration order. Counts retain their unsigned range. Empty native extrema keep
an empty selection of the same kind; constructed min([]) still returns <>.
Set expressions keep operand order. For declarations whose written references
yield C, A, C and B, A, this program produces names C, A, B:
let left = items | filter(name("left")) | references | declarations;let right = items | filter(name("right")) | references | declarations;(left union right) | namesleft intersect right yields A; left except right yields C. These forms
use native identities and remove repeats without moving their first occurrence.
Native selections, strings and semantic rows support the algebra; projected
literal values, counts and dictionaries need their own explicit operations.
All ordering and set forms are read-only. Unsupported kinds and impure selectors
fail before execution, including empty inputs and unused branches. Tally
objects keep their keys in first-seen order too: z, a, z produces
{"z":2,"a":1}.
Rank a native dictionary
Section titled “Rank a native dictionary”map_by retains one payload for each selected item under a string key. Save
this model as dictionary.mzn:
int: A = 1;int: B = 2;int: C = 3;int: reads = B + B + A;solve satisfy;This report keeps the two most used declarations:
zincite query --program 'let ds = declarations | filter(name("A") or name("B") or name("C"));ds | map_by(names, uses | count) | sort_by(value) | reverse | head(2)' \ dictionary.mzn{"complete":true,"limitations":[],"result":{"B":2,"A":1}}The shared map_by entry
explains correlated selector scope and duplicate keys. Zincite retains source
handles and ranges in each payload; explicit sources such as items still read
the original input. Dictionary ordering
and collection selection
apply to the result, including native tally dictionaries.
Save the dictionary to use its length or look up a declaration’s count:
let ds = declarations | filter(name("A") or name("B") or name("C"));let counts = ds | map_by(names, uses | count);report (size: length(counts), a: counts["A"], b: lookup(counts, "B"));This reports a size of 3, a count of 1 for A and a count of 2 for B. lookup returns
absence for a missing key; bracket access requires the key to exist.
Functions can accept and return dict<Node, int> or
dict<string, sequence<Node>>. Native entries retain their source owner and
coordinates, and an empty result retains its declared key and value schemas.
Constructed dictionary fields keep their existing tagged values inside an ordered JSON object. For example:
zincite query --program 'let made = (z: (inner: 3), a: 1); made' dictionary.mzn{"complete":true,"limitations":[],"result":{"z":{"inner":{"kind":"integer","value":3}},"a":{"kind":"integer","value":1}}}Arrays, scalar values and nominal members retain their constructed tags. Native count payloads in the first report render as ordinary numbers.
Rows can keep native source subjects while ordering or grouping by an ordinary field:
let rows = [(key: n | names | one, subject: n) | n in items];report sort_by(rows, "key");report group_by(rows, "key");The reports retain each subject’s original input and source range. Sorting is stable. Groups follow first-seen key order, and their items keep incoming order. Inspect the native report before choosing an explicit renderer.
Count written calls with a function
Section titled “Count written calls with a function”A named function can express a source traversal that you want to reuse or
adapt. Save this input as calls.mzn:
int: first_total = sum([1, 2]);int: second_total = sum(i in 1..2)(i);int: largest = max([3, 4]);solve satisfy;Save this query as calls.zq:
fn count_calls(node: Node, target: string) -> int { let here = if (written_call_name(node) default "") = target then 1 else 0 endif; here + sum([count_calls(child, target) | child in direct_children(node)])}let roots = items;(sum_calls: sum([count_calls(root, "sum") | root in roots]))zincite query --program-file calls.zq calls.mznThe result contains sum_calls = 2. Both the ordinary array call and the
generator call count; max does not. For each node, the function checks its
written call name and adds the counts from its direct children. A node with
no call name contributes zero through the explicit default.
For this count alone, the call_names | tally pipeline is shorter. The
function form gives you control over the traversal when adapting the query
to a more specific source pattern. It inspects syntax without resolving or
executing model calls.
fn declares the function name, typed parameters and the result type after
->. Its body can bind local values with let; the final expression supplies
the return value. A conditional chooses one value and must include else.
Functions are pure: they can inspect source and calculate values, but cannot
edit data or emit output.
Function arguments and results are checked against the declared types.
Recursion is limited by depth and total work. Zincite’s
function reference covers scope, signatures
and limits. For more scalar examples, see Zirql’s
functions and control-flow guide.
Node, Declaration and the source-inspection operations here belong to Zincite.
A sequence of present nodes also satisfies sequence<opt Node>. This example
passes a constructed node sequence to a function that uses an optional-element
queue:
fn first_present(nodes: sequence<opt Node>) -> bool { let present((first, _)) = pop_front(queue<opt Node>(nodes)); occurs(first)}let nodes = [node | node in items];report first_present(nodes);On calls.mzn, the result is true. The function and queue check every node’s
owner and source coordinates. An empty sequence retains its element declaration;
an empty sequence<Declaration> cannot satisfy sequence<opt Node>.
Walk declaration references
Section titled “Walk declaration references”A custom worklist can follow declaration references while skipping duplicate
routes and cycles. Save this source as worklist.mzn:
int: A = if true then B else C endif;int: B = D;int: C = B;int: D = A;int: E = 0;solve satisfy;This static graph is A → [B, C], B → D, C → B, D → A, with disconnected E.
The conditional contributes both written references. The query inspects those
source relationships; it does not evaluate the cyclic MiniZinc definitions.
Save this program as worklist.zq:
fn reaches((start, target): tuple<Declaration, Declaration>) -> bool { loop (pending = queue([start]), seen = identity_set<Declaration>()) { match (pop_front(pending)) { present((current, remaining)) => { if current in seen { continue (remaining, seen); } else if same_declaration(current, target) { break true; } else { let {targets, ..} = reference_info(current); continue (push_back_all(remaining, targets), insert(seen, current)); } }, _ => break false } }}let starts = declarations | filter(name("A"));let targets = declarations | filter(name("D"));let others = declarations | filter(name("E"));(zero_edges: one([reaches((start, start)) | start in starts]), reachable: one([reaches((start, target)) | start in starts, target in targets]), disconnected: one([reaches((start, disconnected)) | start in starts, disconnected in others]));zincite query --program-file worklist.zq worklist.mznzincite-query --program-file worklist.zq worklist.mznWith a configured standard library, both commands return a complete report with
zero_edges = true, reachable = true and disconnected = false. Native
comprehension elements supply the explicit Declaration arguments. Queue order
preserves target order, and the identity set prevents repeated expansion.
reference_info refuses uncertain adjacency at its source location; an unknown
edge cannot become a false reachability result.
Retain reducer states
Section titled “Retain reducer states”A named reducer can destructure its state parameter. Save this as fold-scan.zq
and run it against worklist.mzn with either query command:
fn step((count, total): tuple<int, int>, item: int) -> tuple<int, int> { (count + 1, total + item)}(folded: fold([2, 3, 5], (0, 0), step), scanned: scan([2, 3, 5], (0, 0), step), empty_fold: fold([], (0, 0), step), empty_scan: scan([], (0, 0), step));folded is (3, 10), and scanned contains (1, 2), (2, 5) and (3, 10).
The empty fold returns (0, 0); the empty scan has no states. Steps run in input
order and must preserve the state contract. The same rules apply to retained
native queue/set states. Those states can be inspected in the library, while
rendering a queue or identity set is a located error.
Rust callers construct these same nodes with Program::from_values. For example,
this typed loop returns 3 without parsing query text:
use zincite_query::{Program, ProgramLimits, ProgramValue, Query};use zirql::{ConstructedValue as V, Located, ValueBinaryOperator as B, ValueExpressionKind as K};fn e(value: K<zirql::Expression<Query, (), ()>>) -> ProgramValue { Located::new(value, 0..1)}let n = || e(K::Name("n".into()));let integer = |v| e(K::Literal(V::Integer(v)));let program = Program::from_values(vec![], vec![], e(K::Loop { states: vec![(Located::new("n".into(), 0..1), integer(0))], body: Box::new(e(K::Conditional { branches: vec![(e(K::Binary { operator: B::Less, left: Box::new(n()), right: Box::new(integer(3)), }), e(K::Continue(vec![e(K::Binary { operator: B::Add, left: Box::new(n()), right: Box::new(integer(1)), })])))], otherwise: Box::new(e(K::Break(Box::new(n())))), })),}), ProgramLimits::default())?;K::Fold { items, initial, step } and K::Scan { items, initial, step } take
boxed value nodes plus a located function name. ProgramFunction carries the
named step and its located parameter patterns and declarations. Native query
leaves use zirql::Expression<Query, (), ()>, just as other typed reports.
Evaluate with the original Input and retained model facts, then inspect
result.emissions()[0].value() before rendering. Tuple/sequence values()
borrow native members; computational len() inspects retained queues and sets.
The library and CLI share preparation, native ownership checks and allowances.
Destructure a report
Section titled “Destructure a report”Record and tuple patterns let a report name the parts of a structured value
without a separate binding for every field. This example adds the numbers in
a pair, keeps its label and includes a source declaration in the result.
Use this calls.mzn for both examples below:
int: first_total = sum([1, 2]);int: second_total = sum(i in 1..2)(i);int: largest = max([3, 4]);solve satisfy;The describe function takes the pair apart in its parameter pattern. The
following let takes the result apart and renames label to title:
fn describe({pair: (a, b), label}: record{pair: tuple<int, int>, label: string}) -> record{total: int, label: string} { (total: a + b, label: label)}let {total, label: title} = describe((pair: (3, 4), label: "sum"));fn keep(nodes: sequence<Node>) -> record{selected: sequence<Node>} { (selected: nodes) }let roots = items | head(1);let {selected: root} = keep(roots);report (total: total, title: title, root: root);zincite query --program-file destructured-report.zq calls.mznzincite-query --program-file destructured-report.zq calls.mznThe report has total = 7, title = "sum" and a root selection containing
first_total. Passing the selection through keep preserves its original file
and source range. In the JSON result, root contains that source information;
total and title use the tagged values described in the
value reference.
Choose an optional value with guarded arms
Section titled “Choose an optional value with guarded arms”Use a guarded match when a report needs different calculations for absent,
negative and positive values. This program uses the same calls.mzn:
let {value: candidate, ..} = (value: 7, ignored: "kept out");report match(candidate) { absent => "missing", present(value) if value < 0 => "negative", present(value) if value > 0 => { let (first, second) = (value, 2); if first > second { let result = first + second; result } else { 0 } }, _ => 0,};zincite query --program-file optional-match.zq calls.mznzincite-query --program-file optional-match.zq calls.mznBoth commands report the tagged integer 9. The record pattern binds
candidate to 7; its final .. ignores ignored. The positive arm then adds
2. With <> in place of 7, the first arm would report "missing".
match evaluates its subject once, tries arms in order and runs the first body
whose pattern and guard succeed. present(value) binds a present value, while
absent matches <>. The names first, second and result are local to
their blocks. A final unguarded _ arm is required; here it handles zero.
Both saved programs also work with --program TEXT. For the complete pattern
rules and Rust API, see the pattern reference.
Matching a value is separate from finding source structures with the
match { from ...; bind ...; } capture syntax.
Combine functions, defaults and ordering
Section titled “Combine functions, defaults and ordering”Return to the staffing problem, this time with a missing availability value.
For this report, we deliberately treat missing availability as zero. The goal
is to rank the days by the staff still needed. Save rank-shortages.zq:
fn shortage(required: int, available: int) -> int { max([0, required - available])}let days = [ (day: 1, required: 2, available: 3), (day: 2, required: 3, available: <>), (day: 3, required: 4, available: 3)];let shortages = [ (day: row.day, missing: shortage(row.required, row.available default 0)) | row in days];sort_by(shortages, "missing", descending=true)This program supplies its own data. Run it with empty stdin:
zincite query --program-file rank-shortages.zq < /dev/nullThe result orders day 2 (three missing staff), day 3 (one), then day 1 (zero).
shortage prevents surplus availability from becoming a negative shortage.
The default is an explicit reporting decision, not a claim that the unknown
availability really is zero. If missing data should stop the analysis, use a
requirement instead.
Functions cannot capture top-level bindings, so the required and available counts are passed as arguments. The report leaves the original staffing files unchanged.
Build a dictionary from string keys
Section titled “Build a dictionary from string keys”Bare braces construct a set or a string dictionary in a read-only report.
A colon separates each dictionary key expression from its value. This query
needs no model facts and works with the existing data.dzn fixture:
zincite query --program 'let words=["pear","apple"];{w: {n mod 3 for n in 1..5} for w in words}' \ data.dznThe dictionary keeps pear, then apple; each value is a set iterating as
1, 2, 0. Zincite renders that set with its existing kind and elements
fields inside the complete report envelope. Saving a dictionary retains its
keys, values and insertion order. {w: 1} reads the bound string w;
{"w": 1} uses the literal key w.
Keys can be constructed values, including strings, arrays, tuples, sets and
dictionaries. A duplicate key fails before evaluating its value. Sets compare
by membership and dictionaries by associations, independently of insertion order;
arrays and tuples retain position order. Different kinds remain distinct keys.
Bracket access is strict; lookup(d, key) returns absence for a valid missing
entry. A present absent value remains distinct from a missing entry. General
dictionaries use tagged dictionary JSON entries with explicit keys and values;
string dictionaries retain ordinary object output.
{3, 1, 3} keeps 3, 1, and sort({3, 1, 3}) keeps the set kind in order
1, 3. Empty {} is a dictionary. Use set<int>(), set<bool>(),
set<float>() or set<string>() for typed empty scalar sets, and set<value>()
for an empty general set. Report sets can contain mixed and nested constructed
values. Nominal values and empty nominal sets retain their host-supplied identity.
Natural MiniZinc sets, (field: value) records and | comprehensions remain
available. General dictionaries and string, mixed or nested sets remain outside
data edits and .dzn serialization. Native node/declaration containers retain
their existing host-specific support. The common dictionary and typed empty
syntax is report-only; the
data-edit expression rules
retain their existing closed subset.
Render a mixed native report
Section titled “Render a mixed native report”Zincite uses Zirql 0.3.0’s tagged-data codec inside native reports. Against model.mzn
above, run:
zincite-query --program 'report (subject: items | head(1), count: 1);' model.mznThe subject field keeps Zincite’s source descriptor and the constructed
count uses {"kind":"integer","value":1}. Native sequences remain plain
arrays; constructed tuples, sets and arrays retain their tagged kinds. Records
and native string dictionaries keep field order. The complete, limitations
and result envelope belongs to Zincite, including original file coordinates.
A rendering error leaves stdout empty even after an earlier emitter. MiniZinc
source output still uses Zincite’s source-preserving renderer.
Markdown reports
Section titled “Markdown reports”End a read-only report expression with | markdown to print a table. Save this as
report.zq:
[(message: "inspect declaration", subject: items | head(1), nested: [1,2])]| markdown;Run the same program inline or from the saved file:
printf 'int: x;\nint: y;\n' | cargo run --locked --bin zincite-query -- \ --stdin-filepath model.mzn --program \ '[(message: "inspect declaration", subject: items | head(1), nested: [1,2])] | markdown;'printf 'int: x;\nint: y;\n' | cargo run --locked --bin zincite-query -- \ --stdin-filepath model.mzn --program-file report.zqzincite query accepts the same program flags. Each row keeps a native source
selection until rendering; subject becomes its ordinary descriptor in one
compact JSON cell. Constructed scalars and arrays use ordinary Markdown data,
without Zincite’s tagged JSON wrappers. The
shared layout contract
defines column order, escaping, nested cells and empty results.
Markdown is a terminal emitter. Bindings, pure functions and data-edit programs
cannot emit it. It writes only report data; completeness and limitations remain
available on ProgramResult and through the CLI’s existing diagnostics and exit
status. Evaluation and rendering finish before any bytes reach stdout. The
library reference shows the same report
through Program::parse, evaluate and render.
Explicit scalar conversions in reports
Section titled “Explicit scalar conversions in reports”Project a native selection to its count before converting it to text. This pure
function adds a label to an ordinary report over calls.mzn from the destructuring
example above:
fn label(total:int) -> string { "items=" ++ to_string(total) }let total = items | count;report (label: label(total), parsed: parse_int("002"), price: parse_float("1.25e1"));zincite query --program-file conversion-report.zq calls.mznzincite-query --program-file conversion-report.zq calls.mznThe parsed quantity is 2 and the price is 12.5. The
shared scalar reference
defines the decimal grammar, finite limits and formatting. These calls use the
shared evaluator in reports and pure functions. They are unavailable in Data/edit
expressions, including unused bindings and unselected branches. Native ownership, model facts
and edit safety remain Zincite responsibilities.
Retained tally keys
Section titled “Retained tally keys”Unique and tally follow Zirql’s shared structural identity contract.
A native node key retains its source and range; equal declaration names do not
merge different native subjects. Project names before tally when counting
spellings. Literal value streams compare their actual data.
let hist = items | tally;let saved = hist;report saved | reverse | head(1) | check(1) | sort_by(value);The result keeps the actual source selection as its key through saved bindings and lookup. Non-string keys render as dictionary entry pairs. String tallies retain ordinary dictionary output. The Data edit subset remains closed.