Query expressions and data construction
A query program can calculate values from MiniZinc data and either return a
JSON or Markdown report, or construct a new .dzn file. It reads literal assignments,
binds their values with let, and uses expressions and comprehensions to build
the result. It does not evaluate the MiniZinc model or run a solver.
This page defines the supported expressions, result formats and edit rules. Start with query reports or data edits for a walkthrough. The doubled-instance example below shows how to copy records while updating their indices and references together.
A MiniZinc expression can parse correctly and still be unsupported in a query. Zincite uses Zirql to evaluate supported expressions; its value guide provides additional examples across hosts.
Program declarations and lexical statement order use Zirql’s common reader.
Host admission still applies to every Data/edit expression, including unused
branches and functions: sharing the reader does not widen this closed subset.
Source coordinates, quoted MiniZinc names, original input ownership and atomic
emit; batches remain Zincite responsibilities. The query guide
lists the supported statement surface.
Combine source selections in a report
Section titled “Combine source selections in a report”Source selections and semantic queries can feed a report directly. Use
project(subject, {fields}) to calculate fields for each selected item, or
match { from ...; bind ...; where ...; } to relate declarations to their uses.
The projection examples
show both forms. Selections keep their declaration identity and source
locations throughout the report.
Zirql owns value sources, projection and common native composition: filters, matches, ordering, maps, existence, fixed points and paths. Preparation checks all written bodies against their exact input kinds and lexical scopes, including empty reports. Projection fields and selectors receive singleton subjects; existence relations use each singleton, while fixed points use the whole current selection. Match sources and guards see the original query input and earlier captures. Native bodies retain their actual native result kinds.
Zincite supplies native operations and predicate leaves, selection/scalar views, identity, shape, model facts and complete-observation policy. The host retains source ownership, report envelopes and Data edit safety. Native handles stay native until an explicit projection or rendering request.
A guard that remains unknown fails at the model location responsible. A known true alternative can still keep a row, with any analysis limitations attached. Empty matches keep their field names so later projections can be checked. When a nested operation requires a complete observation, Zincite refuses an incomplete incoming selection before the value or projection runs. Other report transitions retain its incomplete-selection evidence and located limitations.
paths(referenced_declarations, to=predicate, max_depth=n) finds connections
between declarations. Each result contains a source, a sink and the declarations
along one shortest path between them. The path reference
defines the depth bound, traversal order and incomplete-result behaviour.
These operations are available in read-only reports. Data-edit programs support the narrower set of input stages and expressions below.
Common brace forms in read-only reports
Section titled “Common brace forms in read-only reports”Read-only reports use Zirql’s dictionary and set forms,
comparison operators
and comprehensions.
Both [head for pattern in source if condition] and MiniZinc
[head | pattern in source where condition] use the common reader and evaluator.
Natural MiniZinc records keep (field: value) syntax. Native CST leaves retain
source identity; ordinary data in native wrappers follows the shared operators.
Empty {} is a report dictionary. Typed empty sets and mixed/nested sets are
report forms; nominal members and empty nominal sets need host evidence.
value, dict<K,V> and set<value> are available in function declarations.
These report forms do not extend the closed Data subset or .dzn serialization
permissions below.
Dictionary declarations check retained schemas before entries, including empty
and nested dictionaries. Declarations such as dict<Node, int> and
dict<string, sequence<Node>> admit native keys and values, retaining their
source owner and coordinates. Ordinary wrapped data uses the shared constructed
checks.
For example, this counts names while retaining the string-key schema when no
items remain:
fn counts(roots: sequence<Node>) -> dict<string, int> { roots | names | tally}report counts(items | head(0));The empty result satisfies dict<string, int>. Passing it to a
dict<int, int> parameter fails even though there are no keys to inspect.
Shared reader routing and names
Section titled “Shared reader routing and names”Zirql distinguishes constructed ordering calls such as
sort_by([(score: 2), (score: 1)], "score") from native selectors such as
items | sort_by(names). A comma at the call’s own argument level selects the
constructed form; nested calls, strings, quoted identifiers and comments retain
their existing meaning. Saved values and constructed function results use the
same suffix grammar, including dictionary continuations after tally.
Declared result types are available before function bodies are read, so forward
calls use the same suffix grammar.
fn rows() -> sequence<int> { [3, 1, 2] }report rows() | map(x => x + 1) | windows(2);This reports [[4, 2], [2, 3]], matching the equivalent literal or let binding.
Constructed calls can continue into native stages such as min, max and
json; native selector arguments keep their existing syntax.
Binding names use the common reader’s identifier spelling and Zincite’s
MiniZinc identifier rules. Contextual names such as if, match and present
remain available where admitted; quoted names retain their logical identity.
The program reserves let, report, json, markdown, emit, create, replace and
emit_document in binding positions. Function declarations also reject common
operation names and Zincite’s native operations. MiniZinc source grammar,
nominal member lookup, native ownership and the closed Data/edit policy remain
Zincite responsibilities.
Transform report rows
Section titled “Transform report rows”Zincite 0.5.0 uses Zirql 0.3.0 for these report transformations.
Use the shared entries for map and flat_map, selective comprehensions, windows and runs, record updates and alignment.
For example, run this from the Zincite 0.5.0 checkout:
printf 'x = 1;\n' | cargo run --locked --bin zincite-query -- \ --stdin-filepath data.dzn --program 'report { "pairs": [n for case present(n) in [null, 1, 2]] | map(n => n + 1) | windows(2), "sizes": [{"id": 1}, {"id": 1}, {"id": 2}] | runs_by("id") | map(length(_.items)), "changed": {"id": 1, "amount": 2} with {amount: 3}, "keys": align([{"id": 1}], [{"id": 2}], by="id") | map(_.key) };'Program::from_values accepts the corresponding shared ValueExpression
variants, including nested native selections beside scalar fields. Zirql owns
payload mapping, child enumeration and report evaluation; Zincite supplies
native record construction, MiniZinc facts and source coordinates. Written
admission includes every fallback and branch. Eager native discovery leaves
default fallbacks deferred. These operations and selective generators remain
report-only, including in unused bindings and unselected Data branches.
Predicate subjects and patterns
Section titled “Predicate subjects and patterns”rows | filter(row => row.weight > limit) and
rows | filter(_.weight > limit) keep the same elements as
[row for row in rows if row.weight > limit]. The native predicate positions
use the same Boolean, type, purity and lexical-scope rules. A native filter
tests its singleton input; a path target tests the candidate declaration; a
native match guard tests the original query input and sees its separate named
captures. Nested predicates rebind _. Explicit outer names remain captures.
Binding, function/predicate argument, generator and value-match patterns use Zirql’s shared destructuring rules. Native handles, sets and keyed arrays need an established view or explicit conversion before positional list matching.
Scalar element edits accept filter_elements(value => value > 0) and
filter_elements(_ > 0). These bodies use the data-edit operators and helpers
listed below, with distinct scalar and member values and complete candidate checks. General
predicate syntax does not admit report-only helpers, declared functions,
effects or unsupported nominal facts. Direct eq(member("A")) comparisons
retain their existing identity rule.
Report expressions and edit grammar
Section titled “Report expressions and edit grammar”Read-only requests may interleave parameters, immutable bindings and output
expressions. Names become visible in statement order. let binds both input
values and calculations. For a data assignment required = [2, 3, 4];:
let required = items | filter(name("required")) | values | one;sum(required)items selects top-level input items, filter keeps the named assignment,
values reads its literal RHS, and one extracts exactly one projected value.
Zero or multiple matches fail. Written array keys and declared axes or nominal
identities survive the conversion; use --model for declaration evidence in
.dzn reports. Computed RHSs are refused.
Expressions outside bindings emit their results. Native selections keep source
output and native counts print as numbers; calculated values and records produce
JSON. The final expression’s semicolon is optional. report expression;
explicitly emits JSON for a native result, including intermediate inspection
results. A terminal | markdown suffix selects a table for read-only report data;
see Markdown reports. Native subjects retain their
ordinary descriptor cells; constructed data uses the shared Markdown layouts.
This emitter is unavailable in bindings, pure functions and data-edit programs. Use let for an intermediate calculation that should not print.
A data request ends in one atomic edit batch and cannot include output
expressions. Input reads and calculations may interleave before the batch;
every read uses the original input. Native data reads support items, filter,
head, range, values, names, count, unique, fields, elements and
keys, followed by shared one when exactly one projected value is needed.
Semantic filters, project, match and paths remain report-only. Calculations
and native predicate arguments retain the closed data-expression policy below;
report-only calls are refused even in unused bindings or unchosen branches.
Each visible binding and parameter name must be unique. Parameters follow the
required input rules. The grammar below
covers basic report statements and data edits; read-only patterns and
functions have additional syntax.
request = (parameter | binding | output ";")* output [";"] | (parameter | binding)* edit-batch ;parameter = "param" ascii-ident ":" ("int" | "string" | "bool") ";" ;binding = "let" ident "=" expression ";" ;output = ["report"] expression ;edit-batch = edit+ ("emit" | "emit_document") ";" ;edit = ("create" | "replace") ident "=" expression ";" | "append" ident "=" native-source-expression ";" | "scale_enum" ident "=" expression ";" ;
array-comp = "[" expression "|" generators "]" ;indexed-comp = "[" key ":" expression "|" generators "]" ;set-comp = "{" expression "|" generators "}" ;key = expression | "(" expression ("," expression)+ ")" ;generators = generator ("," generator)* ;generator = names "in" expression ("where" expression)? | binder "=" expression ("where" expression)? ;names = binder ("," binder)* ;binder = ident | "_" ;ident = MiniZinc identifier or quoted identifier ;ascii-ident = ASCII letter or "_", followed by ASCII letters, digits or "_" ;create and replace calculate ordinary data values. append consumes opaque
native copies from the stages described under source copying.
scale_enum takes a positive integer scalar expression and changes a supported
enum constructor’s literal upper bound. append and scale_enum require model
facts and are available since Zincite 0.1.2.
The brackets, key prefix and generator forms match the pinned array and
indexed array comprehensions.
A generator’s in source must be a finite constructed or source-backed array,
a fixed set with the supported element types below, or a finite integer range.
Sets iterate in first-insertion order. Explicit sort uses false before
true, numeric order for numbers and declaration order for members. An in generator yields values;
a in records does not yield keys or (key, value) pairs. Use index_set(records) and
records[i] for rank-one keys, or entries(records) to read full keys and values
from any keyed rank. Multiple names before in each range over the
same source, with the last name varying fastest. A single-name = generator
evaluates its right side once for each preceding binding. Earlier generator
bindings are visible in later sources and filters; a generator’s own bindings
are visible in its where and the head. They are not visible in that
generator’s source. The rightmost generator is innermost. _ discards a value
and cannot be referenced. Duplicate bindings, shadowing a source, let or
visible parameter, and forward references fail at the name. Filters require a
fixed Boolean.
An absent value cannot act as a filter; decision-dependent Booleans remain unsupported. These
scope and iteration rules follow the specification’s generator
rules.
Calculated values support the following MiniZinc 2.10.1 expressions:
signed integer, decimal float, Boolean, string and absent (<>)
literals; bound names; parenthesized expressions; field access (r.field),
tuple access (t.1), array access (a[i] or a[i,j]); finite integer ranges
(lo..hi); set, tuple, record, positional array and fully keyed array
literals; the three comprehensions above; and if ... then ... elseif ... then ... else ... endif. The else is mandatory in this subset. Records use
MiniZinc labels, for example (weight: 7, peer: 2); a label is not a string or
member. Both reports and data edits accept index_set(a) for a one-dimensional
array’s keys and enum_member(EnumName, "MemberName") for a declared enum member.
enum_member requires a unique model enum declaration and a
member already declared there. Its arguments are an enum identifier and a
string literal, not computed expressions. Written member references in source
literals retain their resolved identity when the model establishes one. Member
names and generated assignment names must have valid MiniZinc identifier
spelling (quoted when required). Serialization uses the declaration’s member
spelling; it never replaces matching text inside strings or field labels.
Reports additionally accept length(a), sum(a), forall(a), exists(a),
require(condition, value, "message"), group_by(rows, "field"), entries(a),
zip_exact(a,b), contains(text, fragment), starts_with(text, prefix) and
ends_with(text, suffix), plus report ordering and optional extrema linked below.
The scalar and string entries,
reductions,
requirements,
ordering and extrema,
grouping,
entries and
exact pairing
define their signatures, order, empty results and errors. The
ordering report,
machine groups and
matrix comparison provide MiniZinc inputs
and executable programs. Generator-call shorthand such as
forall(i in 1..3)(...) remains unsupported in query expressions.
Read-only keyed operands may have noncontiguous axes and missing cells. Helpers
retain their stored keys; they do not fill holes. Data edits still require
contiguous axes and rectangular coverage. An empty indexed delta comprehension headed by
e.key cannot infer axes after entries; empty-axis inference needs simple
generator key names. Report zip_exact directly to retain declared empty axes.
The matrix example computes nonempty
cell deltas explicitly.
These calls are reporting operations only. An edit program refuses them in
preceding let bindings, including unused lets and skipped branches, and in every
create or replace right-hand side. The only calls allowed in data expressions
are index_set and enum_member.
The edit rules below apply to the whole program, including its bindings.
The following operator list defines the closed MiniZinc Data subset. The shared operator entries own execution, comparison and access semantics; concatenation also accepts positional arrays. MiniZinc spellings and the allowed data shapes below remain Zincite admission rules.
| Form | Accepted operands and result |
|---|---|
+x, -x |
Numeric sign; checked for integer overflow. |
not x |
Boolean negation. |
*, +, - |
Two integers give a checked integer; two floats give a finite float. Mixed numeric operands require exact integer-to-binary64 conversion and give a float. |
div, mod |
Two integers, with MiniZinc’s truncation-toward-zero quotient and remainder sign of the dividend. |
/ |
Two floats, or exact numeric promotion to floats; finite result only. |
<, <=, >, >= |
Two compatible numeric values, or two integer keys. Boolean result. |
=, != |
Equal types: numeric values, Booleans, strings, resolved members of the same enum, or recursively compatible constructed values. Boolean result. |
in |
Scalar membership in a supported finite set or integer range, with compatible element type. |
/\, \/ |
Booleans; evaluate the right side only when needed. |
++ |
Two strings, or two positional one-dimensional arrays; arrays receive fresh 1..n positions. No implicit concatenation of keyed arrays. |
Array and record access evaluates only an existing key or field. Tuple access
uses a positive one-based position. if and elseif evaluate conditions in
order, then only the chosen branch. A false where skips the head. An
unevaluated branch cannot suppress a parse, type or unsupported-form error;
those checks run before evaluation. At runtime a skipped branch does not
produce arithmetic or missing-key errors. Report sets remove duplicates by
structural identity and retain first-insertion order. Data edits retain their
homogeneous fixed Boolean, signed-integer, finite-float or resolved-enum-member
set subset. String sets, nested or mixed sets and general dictionaries are
refused at the data boundary; report support does not imply .dzn serialization
support.
Model calls, arbitrary built-ins, generator calls, interpolation, annotations,
let inside a MiniZinc expression, decision values,
floating ranges and every unlisted operator fail with a located explanation.
Named query functions are the exception to the arbitrary-call exclusion.
They declare typed parameters and a result type, and can use local let
bindings, conditionals and comprehensions.
Function parameters and immutable bindings can destructure tuples and records.
Guarded match(subject) expressions and if ... { ... } else { ... } blocks
choose which value to calculate. Only the chosen body runs, although every body
is checked for valid names, types and allowed operations. Native fields keep
their declaration identity and source coordinates.
Reports also support explicit-state loop, break and continue, named
fold/scan steps, typed queues and native identity sets. These calls compose
inside MiniZinc records, calls and comprehensions. The same scope and type checks
apply to nested inputs, and native values keep their owners and source locations.
These forms are report-only, including in unused bindings and skipped branches. See the
patterns reference and
complete examples.
Query functions and the five native functions use Zirql’s ordinary calls and share the program’s work and result limits. They do not execute model functions. Native owners, source coordinates and model completeness remain Zincite checks. Data-edit programs reject function declarations and calls, even when unused.
Numbers use signed 64-bit integers and finite binary64 decimal floats. Integer
literals may use MiniZinc decimal, 0x or 0o spelling, including
-9223372036854775808; hexadecimal floats are unsupported. Integer
arithmetic, range endpoints and index arithmetic fail on overflow. div,
mod and / fail on zero divisors; i64::MIN div -1 and the matching mod
case fail on overflow. A nonzero decimal literal that rounds to zero fails,
as does any nonfinite literal or result. Conversion of an integer to a float
fails if binary64 cannot represent it exactly. The same rule applies to
filter_elements numeric comparisons. Floating arithmetic also refuses a
nonzero finite mathematical result rounded to zero. No implicit string-to-number
or string-to-member conversion is allowed.
Constructed pipelines
Section titled “Constructed pipelines”Report values use Zirql’s collection stages
and ordering, including
continuations on saved values and tally dictionaries. The comprehension pipe
stays inside brackets; suffix pipes consume its completed result. String arrays
also accept the compact filter(starts_with("a")) spelling. Use
[e.value | e in entries(a)] to project keyed cells to a positional sequence.
These operations remain report-only in unused lets and skipped Data branches.
Continue a calculated array combines
native inputs and report values.
Optional read-only values
Section titled “Optional read-only values”The shared presence and lookup reference
defines occurs, absent, lookup, has_key, lazy default and strict access.
Zincite spells absence <> and renders it as {"kind":"absent"} in tagged data.
Records emit stored fields and keyed arrays emit stored cells; a stored absent
cell remains distinct from an omitted cell.
Retained model declarations constrain field names, optionality, index types and
nominal identity. Decision values and computed source expressions remain
unsupported. Rust callers can inspect ConstructedValue::data() while retaining
its declaration. Data requests refuse absence and optional sources, targets and
values, including in unused lets and skipped branches. External parameters
remain required int/string/bool inputs. The
optional report demonstrates
these distinctions with a model and sparse data.
Constructed shape and result
Section titled “Constructed shape and result”[value | ...] has positions 1..n in generation order, including after a
filter. Fully keyed [key: value | ...] carries one integer or resolved enum
key per dimension. A tuple key establishes its arity. Keys must be unique;
integer axes retain numeric order and enum axes retain known identity and
declaration order. Read-only arrays may omit axis keys or cells. Omission does
not shift stored keys; data edits require contiguous axes and complete
rectangular coverage. A member name in a keyed source needs a matching model declaration to establish its enum
identity. Matching the spelling alone is insufficient.
Rust callers using Program::from_values can set
KeyedArray::declared_axis_sources or
KeyedComprehension::declared_axis_sources on an existing ProgramValue.
These located expressions supply full ordered row and column domains. Each runs
once, left to right, in surrounding scope before cells or generators. Integer
or member arrays and typed sets preserve unused keys, sparse coverage and typed
empties. Nominal values keep their supplied identity and metadata. A literal’s
concrete axes field and declared sources are mutually exclusive.
Natural MiniZinc comprehensions retain their separate lazy
empty_axis_sources fallback. Simple generator key names can establish empty
dimensions from their source values, evaluated outside their own binders only
after zero cells. Explicit eager declarations cannot coexist with that fallback;
target-derived empty domains do not replace them. The CLI forms use MiniZinc
syntax; axes(...) is a common Zirql reader suffix, not MiniZinc notation.
All source evaluation, domain conversion and retained-domain copies share the
request budget and preserve their original locations.
A starting integer key can determine the following keys by checked addition. Enum starting keys cannot be used to infer a tail. Array axes can come from written integer intervals or explicit enums. Integer interval endpoints may refer to unique fixed integer literals in the input; computed or uncertain bounds are unsupported.
Iteration over a multidimensional array yields its values in row-major
axis order. a[i,j] uses a multidimensional key. a[i][j] accesses a nested
array and may have a different length for each outer value. A nested array is
not silently flattened into a multidimensional array. Serialize two-dimensional
integer axes as a matrix when their established keys and values fit that form;
otherwise use fully keyed literals in row-major key order. More dimensions use
fully keyed literals. Report JSON keeps explicit axes and keys even when .dzn
uses a matrix. Empty positional arrays and sets are valid. Refuse an empty
keyed or multidimensional result when its dimensions, enum identity or target
coverage cannot be established from a declared target or explicit source axes.
An empty fixed-size target still must match its declared axes. Shape and type
checks are target-aware for edits; a report may retain a known empty axis.
Constructed values live in a native value type separate from LiteralValue,
whose file, range and spelling describe written source. A report renders
one JSON object with complete, limitations and result. The result is
a scalar or collection value: {"kind":"integer","value":7} (similarly
float, boolean, string), {"kind":"member","enum":"Guests","name":"A"},
{"kind":"set","elements":[...]}, {"kind":"tuple","elements":[...]},
an ordered dictionary object such as {"weight":{"kind":"integer","value":7}}, or
{"kind":"array","indexing":"positional"|"explicit","axes":[[key,...],...],"entries":[{"key":[key,...],"value":...}]}.
General dictionaries render as {"kind":"dictionary","entries":[{"key":..., "value":...}]}, with recursively tagged keys and values. String dictionaries
retain their ordinary object output. Dictionary keys preserve insertion/current
order recursively; empty general dictionaries have an empty entries array and
empty string dictionaries render as {}. Native map_by(key_query,value_query) uses the same ordered
storage while retaining native payloads. It evaluates key then value against
the same singleton and rejects a duplicate before its value. sort_by(value)
uses structural keys, including mixed kinds, absence and ordinary compounds;
native counts retain their unsigned range. Integer and float kinds have separate
ranks. Sorting keeps current-order
ties. Dictionary reverse, head and tail preserve kind and retained order,
including empty, zero and oversized bounds. Native tally keeps its stored
first-seen keys and counts through these operations. Both selectors and known
unsupported input/key/sort kinds and effects are checked even for empty inputs.
Array keys use the scalar integer/member forms. Positional arrays expose their
1..n keys here too; every axis is ordered. A complete report has
"complete":true,"limitations":[]. A report derived from incomplete semantic
inspection keeps complete:false and its existing located limitation objects.
Newly computed values do not invent source ranges. Source-backed query items
may retain their existing file and range in a separate inspected result.
Reports and data edit requests
Section titled “Reports and data edit requests”These two reports read assignment names. The first returns a list of records; the second keeps an explicit integer key for each record.
let names = filter(kind("assignment")) | names;[(name: n, label: "data:" ++ n) | n in names]let names = filter(kind("assignment")) | names;[i: (name: names[i]) | i in index_set(names)]With just counts = [3, 5]; in the input, the second report returns one record
containing its name. Here is the complete JSON output, with line breaks added
for readability:
{ "complete": true, "limitations": [], "result": { "kind": "array", "indexing": "explicit", "axes": [[{"kind": "integer", "value": 1}]], "entries": [{ "key": [{"kind": "integer", "value": 1}], "value": {"name": {"kind": "string", "value": "counts"}} }] }}Native input pipelines project a Strings result to a positional array of
strings, Count to an integer, and Literals to a positional array of
source-backed literal values. For one named RHS, use
items | filter(name("counts")) | values | one; one checks the projection’s
cardinality and extracts the value. Item, node and semantic selections need an
explicit value projection before use in a calculation. Histograms and rendered
JSON or data documents cannot be used as calculation inputs. Reports derived
from semantic queries keep their completeness status and limitations. A
projected name is a string; its spelling does not make it an enum member or a
record label.
For a .dzn containing counts = [3, 5];, with counts and
doubled_counts both declared as array[1..2] of int in a read-only model,
these requests make different kinds of data edit:
let old_counts = items | filter(name("counts")) | values | one;create doubled_counts = [old_counts[i] * 2 | i in index_set(old_counts)];emit;let old_counts = items | filter(name("counts")) | values | one;replace counts = [old_counts[i] + 1 | i in index_set(old_counts)];emit;The first adds doubled_counts = [6, 10];; the second replaces counts with
[4, 6]. Neither is equivalent to set_value("[old_counts[i] * 2 | ...]"),
which only inserts expression text. create requires an absent assignment
name and a unique compatible model declaration. replace requires exactly one
existing assignment. Duplicate targets fail; a request cannot create and
replace the same name. All bindings read the original input,
never a preceding edit in the same request. Creation appends a complete item
after the last assignment, before trailing detached comments, using the input’s
line ending. An edit never changes model or include files. If the model does not
uniquely identify the target declaration and its type and indices, Zincite cannot
validate the assignment and refuses the edit.
An edit program constructs one atomic batch. It checks supported target types,
written finite integer domains and array shapes against the model declarations
and the combined result of all edits. The doubled example requires entities
length 4, groups length 2 and
capacities shape 4 × 2, while input bindings still read the original
2, 1 and 2 × 2 values. Array bounds can depend on supported integer literal
assignments. A computed, ambiguous or unresolved bound stops the whole batch at
the relevant source location. These checks do not establish full MiniZinc type validity or
satisfiability.
Replacement changes only the original right-hand side. A right-hand side
containing comments cannot be replaced automatically; move or remove those
comments explicitly before making the edit. Comments outside replaced ranges,
whitespace and all other source bytes stay in place. Created items have no
inherited comments; creation fails if its placement would attach a directive to a different item.
Zincite rejects overlapping edits and changes that would damage a directive,
then reparses the complete .dzn candidate before previewing or writing it.
An error leaves stdout empty and the original file unchanged.
Copy existing enum-backed data
Section titled “Copy existing enum-backed data”A collection with comments often needs to grow without rebuilding its values. Source-copy stages retain those bytes and change only established enum member and key tokens. These operations are available since Zincite 0.1.2.
The small model declares
People, an array of person records, group sets, and integer weights. Its
data file defines
Alice and Bob, with comments on their records. Each record contains a display
label, an unrelated colour enum, and a set of friends. The
complete query grows all
four assignments explicitly:
param factor: int;let additional = factor - 1;let prefix = "Copy";let copies = items | filter(name("People")) | values | clone_members(additional, prefix);append People = copies;append people = items | filter(name("people")) | values | clone_elements(copies);append groups = items | filter(name("groups")) | values | clone_elements(copies);append weights = items | filter(name("weights")) | values | repeat_elements(additional);emit;With factor=2, the new members are Copy_1_1 and Copy_1_2. Copied person keys,
friend references and group members use those names. Display labels, colour
members and comments keep their original bytes. factor=1 makes no additional
copies and returns the exact original file. Counts and prefixes use ordinary
shared scalar expressions and the current parameters and immutable bindings.
The stages take a native literal stream directly, without one. one extracts
an ordinary value for calculations; copying needs the selected source literal
and its retained model type.
clone_members(additional_count, prefix) requires one nonempty literal member-set
assignment and its unique uninitialized model enum. The prefix must start with
an ASCII letter and contain only ASCII letters, digits and underscores. Generated
names use the prefix, one-based copy number and one-based written member position.
A collision with input identifiers, model/include declarations or another member
appended in the batch refuses the request. The mapping can be reused by several
clone_elements(copies) stages.
clone_elements(copies) and repeat_elements(additional_count) each require one
original literal set or rank-one array assignment. Copies follow copy order, then
original element order. Records, tuples, sets and nested rank-one arrays retain
their comments and spelling. clone_elements remaps only tokens whose retained
model type establishes the mapped enum; labels, strings and other enums are
unchanged. repeat_elements does no remapping or explicit-key reindexing.
Duplicate array keys therefore fail validation. Starting-key arrays, matrices,
computed values, optional types and copied formatter/lint directives are refused.
append Target = expression; requires one original assignment. Member copies
must append to their own enum; other copies must match the target’s collection
kind and declared type. Each target is explicit. Zincite does not clone dependent
assignments automatically. The final check includes unchanged data assignments
whose types or axes use a changed enum, so omitting append people above fails
on array coverage. It checks supported literal types, numeric domains, enum
membership and array keys against the complete final batch. Array domains are
full enums, bounded integer ranges, or unconstrained integer indices. Enum-valued
elements must use the full enum domain, directly or through a type alias; subset
domains are refused, including inside arrays, records, tuples and sets.
Model-derived values, decision arrays, constraints and solve items are not evaluated.
scale_enum Target = factor; grows a constructor-backed enum with exactly one
original assignment and one unique uninitialized model enum declaration. The
assignment must have the form C(1..N), where C is a named constructor and N
is a positive integer literal. The factor is a positive integer expression using
the current parameters and immutable bindings. Zincite checks multiplication for
overflow and changes only the upper-bound token to the calculated integer.
Factor 1 preserves the original bytes.
For example, scale_enum Tables = 2; changes Tables = T(1..2); to
Tables = T(1..4);. A positional array[Tables] assignment must contain four
entries in the final candidate, even if the query leaves that assignment
unchanged. Use repeat_elements and an explicit append when the new tables
should repeat the original capacities. Other constructor forms, computed bounds
and constructor-member references such as T(1) outside the defining assignment
are refused. The bounded check uses the enum’s cardinality; it does not evaluate
constructor members or general MiniZinc expressions. The
wedding walkthrough combines guest
member cloning and table scaling in one complete program.
Copy stages are available only in programs with model facts and a terminal
emit;. Their results are opaque native source values, consumed by append;
they cannot be emitted or used as constructed values. All reads use the original
input. Duplicate targets across append/scale_enum/create/replace fail. A constructed edit
whose type uses a changed enum is refused; unrelated supported constructed edits
can share the batch. Ordinary create, replace and enum_member keep their
existing rules.
The same Program::parse and evaluate_with_model_and_parameters Rust entry
points execute this query. The resulting ProgramResult::candidate contains the
complete reparsed document. Both CLI forms support preview, --diff and --write
with their usual buffered output and file checks. Copied source is data growth;
it does not establish model satisfiability.
A complete doubled instance
Section titled “A complete doubled instance”The input below has two entities in one group. Each entity stores a weight, the index of its peer and the index of its group. The two entities refer to each other. Each also has a two-column capacity row.
We want two independent copies of this instance: four entities in two groups. The copied entities need new indices, and their peer and group references must point into their own copy. Changing the counts alone would leave arrays and references inconsistent.
Save the model and data as these two files:
int: entity_count;int: group_count;array[1..entity_count] of record(int: weight, int: peer, int: group): entities;array[1..entity_count, 1..2] of int: capacities;array[1..group_count] of set of int: groups;solve satisfy;entity_count = 2;group_count = 1;entities = [(weight: 7, peer: 2, group: 1), (weight: 11, peer: 1, group: 1)];capacities = [| 3, 4 | 5, 6 |];groups = [{1, 2}];The query uses two index maps. Copy 0 keeps the original indices. Copy 1 maps entities 1 and 2 to 3 and 4, and group 1 to group 2. The peer of copied entity 3 is therefore 4, rather than original entity 2.
Save the program as double.zq. Its first three bindings read the original
records, capacity matrix and group memberships. The replacements update the
counts and all three collections in one batch.
let old_entities = items | filter(name("entities")) | values | one;let old_capacities = items | filter(name("capacities")) | values | one;let old_groups = items | filter(name("groups")) | values | one;let entity_map = [(i,c): i + 2*c | i in 1..2, c in 0..1];let group_map = [(g,c): g + c | g in 1..1, c in 0..1];replace entity_count = 4;replace group_count = 2;replace entities = [ (weight: old_entities[i].weight, peer: entity_map[old_entities[i].peer,c], group: group_map[old_entities[i].group,c]) | c in 0..1, i in 1..2];replace capacities = [ (entity_map[i,c],j): old_capacities[i,j] | c in 0..1, i in 1..2, j in 1..2];replace groups = [ group_map[g,c]: {entity_map[i,c] | i in old_groups[g]} | c in 0..1, g in 1..1];emit;| Old key | Copy 0 | Copy 1 |
|---|---|---|
| Entity 1 | 1 | 3 |
| Entity 2 | 2 | 4 |
| Group 1 | 1 | 2 |
With the MiniZinc standard library configured, run:
zincite query --max-work 5000000 --model doubled.mzn -f double.zq doubled.dznThis example raises the work allowance because loading the standard library
and checking several dependent assignments can exceed the default budget.
The command prints the complete candidate without changing doubled.dzn:
entity_count = 4;group_count = 2;entities = [(weight: 7, peer: 2, group: 1), (weight: 11, peer: 1, group: 1), (weight: 7, peer: 4, group: 2), (weight: 11, peer: 3, group: 2)];capacities = [| 3, 4 | 5, 6 | 3, 4 | 5, 6 |];groups = [1: {1, 2}, 2: {3, 4}];Add --diff to inspect the changes or --write to apply them. The
data-edit guide also shows how to redirect
the candidate into a separate file.
The weights and capacity rows are copied unchanged. Peer indices become
2, 1, 4, 3, and group indices become 1, 1, 2, 2. The group memberships
are {1, 2} and {3, 4}, so each pair stays together.
A matrix indexed by entities on both axes would need another decision: what should its cells contain for pairs from different copies? That relationship must be written into the query; copying a single entity axis does not define it.
Enum members need an explicit mapping too. Suppose the model
declares enum Guests = {A, B, A_copy, B_copy};. This report binding names
both sides of a separate member mapping:
let guest_map = [ enum_member(Guests, "A"): enum_member(Guests, "A_copy"), enum_member(Guests, "B"): enum_member(Guests, "B_copy")];The map relates actual enum members. A string "A" or a record field named
A is a different value and is unaffected. Every mapped member must already
be declared in the model; a missing or ambiguous declaration is an error.
Work limits and errors
Section titled “Work limits and errors”One program budget covers reading input, calculations, generator candidates, intermediate values, copying and rendering. Candidates count before filters, so a comprehension with a small result can still do substantial work. The default limits are 64 nested expression or collection levels, 1,000,000 work units and 100,000 aggregate result entries. See program limits for CLI overrides and the separate stream limit.
Source-copy maps, identifier comparisons, token walks, copied fragments, final literal validation and candidate assembly spend the same work/result allowances before expansion. Defaults are unchanged. Reusing a member map retains its native owner; each collection copy and each output assembly still consumes budget.
Repeating an expansion or serialization spends more of the same budget. Reusing an immutable input value avoids copying all of its contents, but creating new values, snapshotting outputs and rendering them still consume work and result entries. Model scanning consumes work; raw model tokens do not count as report entries. A limit error stops the request and produces no truncated report or partially written data file.
For library callers, the accounting rules are:
- Reducers first calculate their complete operand array.
sum,forallandexistsspend one work unit per inspected element.lengthspends one for counting present values and converting the count to an integer. Each summary value uses one result entry, including for an empty input. requirechecks its message and operands before evaluation. A failed requirement spends work on copying its diagnostic message. A successful one returns its child value without another copy charge.- Grouping,
entriesandzip_exactcount field and key comparisons, enum metadata checks, temporary containers and copied keys or labels. Moving an already evaluated row or cell into its result does not deep-copy that value. - Core projection steps prepare their fields and native arguments once per request, before subjects are enumerated. Operations still run for each selected subject. Reusing an input value spends reference and slot work; new input values and changed contracts must be checked again.
- Scope metadata and storage growth consume budget before allocation. Leaving a scope does not refund work. Output snapshots and each rendering spend the remaining budget, including repeated library calls to render the same result.
Errors point to the responsible query expression or original model/data
location. These include invalid syntax, unsupported operations, incompatible
types, missing keys, duplicate targets and arithmetic errors. A semantic report
can return known results with complete: false and located limitations. An
unknown filter condition remains unknown under negation and cannot become a
match merely through not.
An edit needs enough information to validate every target. Unknown dependencies, unresolved enum identities and unsupported computed source values stop the batch. Some incomplete reductions can be previewed as described in the enum-reduction reference; in-place writing always requires a complete candidate. An actual evaluation or rendering error leaves stdout empty.
Zirql reads and evaluates query expressions, bindings, predicate bodies, comprehensions and common native composition. Zincite parses input MiniZinc files and supplies source selections, model facts and literal values. Zirql renders ordinary report data; Zincite supplies native descriptors and completeness envelopes, or validates and serializes the complete edited data file. The report and edit forms share that evaluator and budget, while data edits allow fewer operations. Neither form runs a MiniZinc compiler or solver.
Use --model to supply target declarations, --diff to inspect changes and
--write to update an explicit regular data file. Model and include files are
read-only. The Rust reference
describes Program, its result types and candidate handling.
Native ordering and sets
Section titled “Native ordering and sets”The shared ordering reference covers native and constructed forms together. Zincite admits scalar string and literal streams directly; source handles need an explicit projection. Native counts retain their unsigned range, nominal keys retain declaration identity, and empty native extrema retain their selection kind and owner. Selectors see one incoming subject and lexical bindings; explicit input stages still read the original input. The walkthrough retains source nodes while ranking their values.
Native set operators use source identity. Nodes, semantic entities/type/instantiation rows and strings are admitted; literal streams, counts, dictionaries, captures and computational identity sets are not. Singular nodes and declarations contribute one row and set results are selections. Zincite validates every operand’s owner, including discarded rows, and retains completeness evidence through empty results. These operations do not widen the Data subset.
Native existence and fixed points
Section titled “Native existence and fixed points”Native where_exists(relation) and fixpoint(body) are pure report operations.
Their bodies use the current runner, lexical scope and continuing request
budget. Preparation checks known kinds, names and effects even when the input
is empty. Existing data-expression permissions remain unchanged.
Existence keeps original subjects in input order, retaining duplicates. Its relation runs against each singleton and must return an admitted native selection. Complete empty results reject a subject; uncertain adjacency and final unknown predicates refuse. A saved partial selection retains its responsible error through copies, aliases, ordering and set operations, even when an operation discards all rows. Unrelated report limitations do not refuse a complete observation. Singular nodes and declarations normalize to their possibly empty selection kinds at the existence boundary.
Fixed points replace the selection until ordered identities and duplicate
multiplicity are unchanged. The body keeps the exact native kind, element
contract and source owner, including for empty results. Explicit input union
provides accumulation. Stable empty selections succeed; repeated non-fixed
selections refuse with one bounded checkpoint. Native copying, comparison and
nested evaluation spend the active budget. See the
walkthrough and
reference.
Explicit scalar conversions
Section titled “Explicit scalar conversions”Ordinary reports and pure functions accept the shared parse_int, parse_float
and to_string calls. See the scalar contract
and native count report recipe.
Native selections require an existing scalar projection first. The closed Data subset retains its existing
operator/helper allowlist; conversions are report-only even in unused bindings,
skipped branches and empty comprehensions. They do not change source literal parsing or model-backed
construction rules.
Native unique and tally share the same operation contract. Native leaves retain source owners and coordinates; literal wrappers expose ordinary data for structural comparison. Retained dictionaries keep actual keys through copying, saved bindings and lookup. Their renderer projects native leaves only at the output boundary. These report operations do not widen Data expression or edit permissions.