Rindle docs and package mapSkip to main content

rindle/
change.rs

1//! The dataflow value types: `Node`, the lazy relationship stream, `Change`,
2//! `SourceChange`, `Constraint`, `FetchRequest`, and the overlay snapshots.
3//!
4//! Primitive #6: the `'yield'` sentinel is **dropped**. In the JS engine every
5//! stream element is `Node | 'yield'` (a cooperative-responsiveness hack for the
6//! single JS event loop, stripped before the view by `skipYields`). The port
7//! drops it entirely: `NodeStream`'s item is just `Node`. Responsiveness, if
8//! ever needed, is an explicit budget threaded in — never unioned into the item.
9
10use crate::graph::Graph;
11// `Row` aliases the canonical owned row; the dataflow types are always owned (a
12// `Node` is buffered, spliced with overlays, and handed to the view — none of
13// which a step-buffer borrow could outlive), so there is no `Value<'a>` here.
14use crate::value::{ColId, OwnedRow as Row, OwnedValue, RelId};
15
16/// A lazy, single-call stream of nodes. Boxed `dyn Iterator` because the
17/// pipeline is built from a runtime AST (see README) — operator dispatch is
18/// inherently dynamic; only the leaves can be monomorphized.
19///
20/// `'g` is the graph borrow: streams and relationship thunks hold `&'g Graph`
21/// (a *shared* borrow). Reentrancy works precisely because every fetch is a
22/// shared borrow of the same graph — no `&mut Graph`, no `RefCell<dyn Operator>`.
23pub(crate) type NodeStream<'g> = Box<dyn Iterator<Item = Node<'g>> + 'g>;
24
25/// A lazy stream of **owned rows** — the currency of the *source* read pipeline.
26///
27/// A source emits rows, NOT nodes: the overlay splice, start/constraint/filter
28/// chain, and k-way merge ([`source_common`](crate::source_common)) all operate on
29/// `RowFlow`. Rows become [`Node`]s only when they cross the **connection**
30/// boundary (`Graph::fetch` on a `SourceConn` wraps each row in a leaf node);
31/// downstream **joins** then attach relationships to those nodes. This keeps the
32/// shared source machinery free of any `Node`/relationship concept (`04` §1.1).
33///
34/// `'g` mirrors `NodeStream` for symmetry and to admit a borrowing leaf (the
35/// SQLite cursor borrows `&'g self`); the memory leaf's flow is effectively owned.
36pub type RowFlow<'g> = Box<dyn Iterator<Item = Row> + 'g>;
37
38/// A row plus its lazily-produced relationships.
39///
40/// Relationships are index-addressed (`Vec`, not a string map) and each is a
41/// **thunk** producing a *fresh* stream per call — mirroring JS
42/// `relationships: Record<string, () => Stream>`. The thunk owns everything it
43/// needs (graph ref + captured-by-value overlay), so it can be invoked at view
44/// time without reading any live operator field.
45pub struct Node<'g> {
46    pub row: Row,
47    pub rels: Vec<Relationship<'g>>,
48}
49
50pub struct Relationship<'g> {
51    /// The resolved relationship slot (foundations §3.4 — index, never a string;
52    /// the name lives in `Schema::relationships[slot]`). The View maps it back to
53    /// a name for output.
54    pub slot: RelId,
55    pub thunk: Box<dyn Fn() -> NodeStream<'g> + 'g>,
56}
57
58impl<'g> Node<'g> {
59    /// A leaf node (source row): no relationships.
60    pub fn leaf(row: Row) -> Node<'g> {
61        Node {
62            row,
63            rels: Vec::new(),
64        }
65    }
66}
67
68/// A downstream incremental change. Mirrors `zql/src/ivm/change.ts` but as a
69/// real tagged union (the JS uses a tuple where slot 2 aliases OLD_NODE /
70/// CHILD_DATA — the enum removes that aliasing footgun).
71pub enum Change<'g> {
72    Add(Node<'g>),
73    Remove(Node<'g>),
74    /// An in-place edit: the row's PK is unchanged but some columns differ. Holds
75    /// both the new node and the old node (`makeEditChange` — `change.ts`). A
76    /// connection with split-edit keys never *sees* this (the source decomposes it
77    /// into Remove(old)+Add(new) before fan-out, §3.6); it survives only for
78    /// connections that don't split.
79    Edit {
80        node: Node<'g>,
81        old: Node<'g>,
82    },
83    Child {
84        node: Node<'g>,
85        /// The relationship slot the child change belongs to (index, not a name —
86        /// foundations §3.4). The View resolves it to a name via the schema.
87        rel: RelId,
88        child: Box<Change<'g>>,
89    },
90}
91
92/// The *kind* of a [`Change`], independent of its payload. Used by the OR
93/// fan-in collapse (`push_accumulated_changes`, `06` §3.5): the dedup decision
94/// keys off the **fan-out's** original change type (was it an Add/Remove/Edit/
95/// Child that entered the fan), so we need to name the type without owning a
96/// `Change`. Mirrors the JS `change.type` string discriminant.
97#[derive(Clone, Copy, PartialEq, Eq, Debug)]
98pub enum ChangeType {
99    Add,
100    Remove,
101    Edit,
102    Child,
103}
104
105impl<'g> Change<'g> {
106    /// This change's [`ChangeType`] discriminant (payload-free).
107    pub fn change_type(&self) -> ChangeType {
108        match self {
109            Change::Add(_) => ChangeType::Add,
110            Change::Remove(_) => ChangeType::Remove,
111            Change::Edit { .. } => ChangeType::Edit,
112            Change::Child { .. } => ChangeType::Child,
113        }
114    }
115
116    /// The row identifying this change's node (the *new* row for an `Edit`; the
117    /// changed-child's row for a `Child`). On a child-side join push this is the row
118    /// whose join key selects the matching parents (`join.ts#pushChildChange`).
119    pub fn primary_row(&self) -> &Row {
120        match self {
121            Change::Add(n) | Change::Remove(n) => &n.row,
122            Change::Edit { node, .. } => &node.row,
123            Change::Child { node, .. } => &node.row,
124        }
125    }
126}
127
128// `SourceChange` — what a *producer* says happened to a base table — lives one layer down, in
129// `rindle-value`, so the CDC capture plane and the wire codec can speak it without linking the
130// engine. Re-exported here at its original path. The derived delta is what stays: [`Change`]
131// above, with its `'g` borrow and lazy relationship thunks, and [`CaughtChange`](crate::CaughtChange).
132pub use rindle_value::change::SourceChange;
133
134/// `(column, value)` equality constraints, ANDed. Index-addressed. Owned values
135/// (`Vec<(ColId, OwnedValue)>`) because a constraint outlives the fetch that
136/// produced its source row (foundations §3.3). Insertion order is significant: it
137/// determines the index sort the source builds to seek the constraint (mirrors
138/// the JS `Object.keys(constraint)` order — `memory-source.ts:289`).
139pub type Constraint = Vec<(ColId, OwnedValue)>;
140
141/// A disjunction of [`Constraint`]s — a row matches the multiConstraint iff it
142/// matches **some** entry. FlippedJoin lowers an IN-batch to these
143/// (`operator.ts` `MultiConstraint`). The memory leaf drives a sub-fetch per
144/// entry and k-way-merges (`#fetchMulti`); the SQLite leaf lowers to native `IN`.
145pub type MultiConstraint = Vec<Constraint>;
146
147/// True if `row` satisfies every `(col, value)` pair. Uses
148/// [`values_equal`](crate::value::values_equal)
149/// (null ≠ null — SQL/join semantics), matching the JS `constraintMatchesRow`
150/// (`constraint.ts:17`). **Not** `compare_values` (which treats null == null):
151/// a constraint value of null can never match (and joins never produce one —
152/// `build_join_constraint` returns `None` on null, §3.8).
153pub fn constraint_matches(row: &Row, c: &Constraint) -> bool {
154    c.iter()
155        .all(|(col, v)| crate::value::values_equal(row.col(*col), v.as_ref()))
156}
157
158/// True if the constraint's columns are exactly the primary-key columns (as a
159/// set). Mirrors `constraintMatchesPrimaryKey` (`constraint.ts:46`): when this
160/// holds and the PK is a single column, the fetch can skip appending the
161/// requested sort to the index sort (there is at most one matching row, §3.3).
162pub(crate) fn constraint_matches_primary_key(c: &Constraint, pk: &[ColId]) -> bool {
163    if c.len() != pk.len() {
164        return false;
165    }
166    pk.iter().all(|p| c.iter().any(|(col, _)| col == p))
167}
168
169/// Merge two constraints, `extra` overriding `base` on shared columns — the
170/// `{...base, ...extra}` of `#fetchMulti` (`memory-source.ts:398`). Order: base
171/// columns first (values overridden in place), then `extra`'s new columns
172/// appended, matching JS object-spread key order.
173/// `constraintsAreCompatible` (`constraint.ts`): are two constraints jointly
174/// satisfiable? Compatible iff **no shared column disagrees** — for every column
175/// present in *both* `a` and `b`, the values must be
176/// [`values_equal`](crate::value::values_equal). Columns appearing in only one
177/// side place no requirement on the other.
178///
179/// Used by [`FlippedJoin`](crate::op::FlippedJoin)'s batched fetch: a child-derived
180/// parent constraint that contradicts the incoming `req.constraint` (e.g. a chained
181/// flipped join passing a parent-key constraint through) could never match, so it
182/// is dropped before it enters the IN-batch (`flipped-join.ts:250`).
183pub(crate) fn constraints_are_compatible(a: &Constraint, b: &Constraint) -> bool {
184    a.iter().all(|(col, va)| {
185        b.iter()
186            .find(|(c, _)| c == col)
187            .is_none_or(|(_, vb)| crate::value::values_equal(va.as_ref(), vb.as_ref()))
188    })
189}
190
191pub(crate) fn merge_constraints(base: Option<&Constraint>, extra: &Constraint) -> Constraint {
192    let Some(base) = base else {
193        return extra.clone();
194    };
195    let mut out: Constraint = base
196        .iter()
197        .map(|(col, v)| {
198            match extra.iter().find(|(c, _)| c == col) {
199                Some((_, ev)) => (*col, ev.clone()), // overridden value, base position
200                None => (*col, v.clone()),
201            }
202        })
203        .collect();
204    for (col, v) in extra {
205        if !base.iter().any(|(c, _)| c == col) {
206            out.push((*col, v.clone()));
207        }
208    }
209    out
210}
211
212/// Build the constraint for a join hop: maps each `to_key` column to the
213/// `from_row`'s `from_key` value. Returns `None` if any value is null (null
214/// cannot join — `values_equal` semantics).
215pub(crate) fn build_join_constraint(
216    from_row: &Row,
217    from_key: &[ColId],
218    to_key: &[ColId],
219) -> Option<Constraint> {
220    let mut out = Vec::with_capacity(from_key.len());
221    for i in 0..from_key.len() {
222        let v = from_row.col(from_key[i]);
223        if v.is_null() {
224            return None;
225        }
226        out.push((to_key[i], v.to_owned()));
227    }
228    Some(out)
229}
230
231#[derive(Clone)]
232pub enum Basis {
233    At,
234    After,
235}
236
237#[derive(Clone)]
238pub struct Start {
239    pub row: Row,
240    pub basis: Basis,
241}
242
243/// What `Input::fetch` receives. `multi_constraints` (from FlippedJoin) is a
244/// disjunction-of-conjunctions IN-batch; empty ⇒ the single-constraint path.
245#[derive(Clone, Default)]
246pub struct FetchRequest {
247    pub constraint: Option<Constraint>,
248    pub multi_constraints: Vec<MultiConstraint>,
249    pub start: Option<Start>,
250    pub reverse: bool,
251}
252
253impl FetchRequest {
254    pub fn all() -> FetchRequest {
255        FetchRequest::default()
256    }
257    pub fn with_constraint(c: Constraint) -> FetchRequest {
258        FetchRequest {
259            constraint: Some(c),
260            ..Default::default()
261        }
262    }
263    /// Mirrors the `#fetch` guard: `multiConstraints?.some(mc => mc.length > 0)`
264    /// (`memory-source.ts:264`). Empty entries don't count.
265    pub fn has_multi(&self) -> bool {
266        self.multi_constraints.iter().any(|mc| !mc.is_empty())
267    }
268}
269
270/// Primitive #5: the JOIN overlay, **snapshotted by value** into a relationship
271/// thunk. In JS the child-stream closure reads the live `#inprogressChildChange`
272/// field mid-push (the borrow checker's nemesis). Here the in-progress child
273/// change is *captured by value* at Node construction, so the thunk owns its
274/// overlay and never touches a shared mutable field.
275///
276/// Carries the in-progress child change as a [`SourceChange`](crate::change::SourceChange) (rows): `Add` splices
277/// the child in, `Remove` splices it out, `Edit` does both (remove old, add new).
278/// `SourceChange` (not the spec-`06` `Change`) keeps the overlay **cloneable** —
279/// the thunk captures a fresh copy per parent — and matches the spike's reality
280/// that an in-progress child change is a leaf source row.
281///
282/// **The production `{change, position}` gate is LIVE (no longer deferred):** both
283/// join twins carry it — [`Join`](crate::graph::Join) as
284/// `inprogress_overlay`/`inprogress_position` read through the gated
285/// `Graph::join_overlay_for`, and [`FlippedJoin`](crate::op::FlippedJoin) as its own
286/// pair read through `inflight_fetch_action` — with the
287/// `compareRows(parent, position) > 0` delivered-yet gate compared in the parent's
288/// effective per-query order. `JoinOverlay` itself stays the positionless by-value
289/// change carrier; the splice polarity is chosen at attach time
290/// (`Graph::attach_child_rel` + `OverlayPolarity`): `Post` re-materializes the
291/// POST-change children (`splice_join_overlay`, the original spike polarity), `Pre`
292/// reconstructs the PRE-change children for a parent the fan-out has not reached
293/// (`splice_join_overlay_pre`). None of this is dead scaffolding: removing the
294/// `inprogress_*` fields or the `Pre` polarity reintroduces the phantom-remove
295/// defect on non-unique parent join keys
296/// (`follow-ups/take-flip-bound-crossing-edit.md`, defect #2).
297#[derive(Clone)]
298pub(crate) struct JoinOverlay {
299    pub change: SourceChange,
300}
301
302impl JoinOverlay {
303    /// The row-level overlay for an in-flight child change, or `None` for a nested
304    /// [`Change::Child`].
305    ///
306    /// A row-level overlay (`Add`/`Remove`/`Edit`) splices a leaf child row into a
307    /// parent's child stream (the spike re-materialize polarity, see the struct
308    /// doc). A nested `Child` returns `None` because no consumer needs the
309    /// overlay-spliced parent stream on a `Change::Child`: the [`View`](crate::view)
310    /// (`ViewChange::from_change`) and the testkit `Catch` (`expand_change`) keep only
311    /// the parent **row** and recurse into the carried sub-change — they never drain
312    /// the parent's relationship thunks. (The one operator that *does* drain a parent
313    /// thunk on a `Child` — [`Exists`](crate::op::Exists), re-counting its gated
314    /// relationship — wants the source-truth size, which the active source overlay
315    /// already supplies; the join overlay would be the wrong input there.)
316    pub(crate) fn for_child_change(change: &Change) -> Option<JoinOverlay> {
317        match change {
318            Change::Add(n) => Some(JoinOverlay {
319                change: SourceChange::Add(n.row.clone()),
320            }),
321            Change::Remove(n) => Some(JoinOverlay {
322                change: SourceChange::Remove(n.row.clone()),
323            }),
324            Change::Edit { node, old } => Some(JoinOverlay {
325                change: SourceChange::Edit {
326                    row: node.row.clone(),
327                    old: old.row.clone(),
328                },
329            }),
330            Change::Child { .. } => None,
331        }
332    }
333}
334
335// ---------------------------------------------------------------------------
336// Owned (no-`'g`) change materialization
337// ---------------------------------------------------------------------------
338//
339// A [`Change<'g>`] borrows the graph (its relationship thunks are `+ 'g`), so it
340// is neither `'static` nor `Clone`. Two places need to hold a change *off the
341// stack* and reconstitute it later:
342//
343//   * the join child-push ([`Graph::push_child_change`](crate::graph)) materializes
344//     the in-flight child change **once**, then rebuilds a fresh `Change<'g>` per
345//     matching parent (a child can join to many parents, each needing its own
346//     forwardable copy); and
347//   * the [`UnionFanIn`](crate::op::UnionFanIn) accumulates each OR branch's output
348//     across a broadcast, collapses, and rebuilds one change to forward.
349//
350// The owned tree is exactly what the `View`/`Catch` already build under the
351// re-materialize model: the thunks run **now** (during the push, while the source
352// overlay is active), capturing the post-change relationship state. No `unsafe`,
353// no `'static`-`Change` field.
354
355/// An owned (no-`'g`) materialization of a [`Node`]: its row plus its fully-drained
356/// relationship subtrees (slot → children). Mirrors the testkit's `CaughtNode`.
357#[derive(Clone)]
358pub(crate) struct OwnedNode {
359    pub row: Row,
360    pub rels: Vec<(RelId, Vec<OwnedNode>)>,
361}
362
363/// An owned change. `Add`/`Remove`/`Edit` carry materialized nodes; `Child` carries
364/// the changed node, the relationship slot, and the owned sub-change.
365///
366/// The `Child` **node** usually carries an empty `rels` (the cheap default — the
367/// View/Catch read only its row and recurse into `sub`, see
368/// `JoinOverlay::for_child_change`). `materialize_change_preserving_node`
369/// instead drains the node's relationships, which the **filter fan** broadcast
370/// needs: an [`Exists`](crate::op::Exists) branch downstream of a `FanOut` counts
371/// its gated relationship off the change's node, so the broadcast rebuild must keep
372/// it (the keystone join path leaves it empty to stay cheap).
373#[derive(Clone)]
374pub(crate) enum OwnedChange {
375    Add(OwnedNode),
376    Remove(OwnedNode),
377    Edit {
378        node: OwnedNode,
379        old: OwnedNode,
380    },
381    Child {
382        node: OwnedNode,
383        rel: RelId,
384        sub: Box<OwnedChange>,
385    },
386}
387
388/// Drain a node's relationship thunks into an owned subtree (recursively). The
389/// thunks run **now**, capturing the post-change relationship state.
390pub(crate) fn materialize_node(n: &Node) -> OwnedNode {
391    OwnedNode {
392        row: n.row.clone(),
393        rels: n
394            .rels
395            .iter()
396            .map(|r| (r.slot, (r.thunk)().map(|c| materialize_node(&c)).collect()))
397            .collect(),
398    }
399}
400
401/// Materialize a [`Change<'g>`] into an owned change (consumes the change).
402pub(crate) fn materialize_change(c: Change) -> OwnedChange {
403    match c {
404        Change::Add(n) => OwnedChange::Add(materialize_node(&n)),
405        Change::Remove(n) => OwnedChange::Remove(materialize_node(&n)),
406        Change::Edit { node, old } => OwnedChange::Edit {
407            node: materialize_node(&node),
408            old: materialize_node(&old),
409        },
410        // A `Child` keeps only the parent row + recurses; the parent node's own
411        // relationships are not consumed by the View/Catch (they read the row and
412        // recurse into `sub`), so the default drops them — the keystone optimization.
413        // The filter-fan broadcast needs them and uses
414        // [`materialize_change_preserving_node`] instead.
415        Change::Child { node, rel, child } => OwnedChange::Child {
416            node: OwnedNode {
417                row: node.row,
418                rels: Vec::new(),
419            },
420            rel,
421            sub: Box::new(materialize_change(*child)),
422        },
423    }
424}
425
426/// Materialize a [`Change<'g>`] for the **filter-fan broadcast**, preserving the
427/// change's node relationships across every variant. A `FanOut` replays one change to
428/// every OR branch; an [`Exists`](crate::op::Exists) branch counts its gated
429/// relationship off the change's node, so the rebuilt copy each branch receives must
430/// carry that relationship — drained **now**, under the active source overlay, so the
431/// count reflects the post-change membership (exactly what the live, non-fanned
432/// `Exists` reads).
433///
434/// `Add`/`Remove`/`Edit` already materialize full nodes ([`materialize_change`]); only
435/// [`Change::Child`] differs — [`materialize_change`] drops a Child's node rels (the
436/// keystone optimization, since the join child-push's downstream View/Catch never read
437/// them), so this overrides that one arm to keep the **top** node's relationships (the
438/// OR branches gate the top node; the nested sub-change rides along via the cheap path).
439pub(crate) fn materialize_change_preserving_node(c: Change) -> OwnedChange {
440    match c {
441        Change::Child { node, rel, child } => OwnedChange::Child {
442            node: materialize_node(&node),
443            rel,
444            sub: Box::new(materialize_change(*child)),
445        },
446        other => materialize_change(other),
447    }
448}
449
450/// Like [`materialize_change`], but an `Edit`'s **old** node is materialized
451/// ROW-ONLY (empty `rels`) — its relationship subtree is drained by no downstream.
452/// [`ViewChange::from_change`](crate::view::ViewChange::from_change) reduces an Edit
453/// to `(row, old_row)`, [`Take`](crate::op::Take)'s edit matrix keys off `old.row`,
454/// and [`Exists`](crate::op::Exists) gates only the **new** node's slot — nothing ever
455/// reads `old`'s children. Draining them is a reentrant leaf refetch of a subtree the
456/// consumer already holds unchanged (the join-key-immutability contract guarantees a
457/// child edit cannot alter this relationship's correlation, so `old`'s subtree equals
458/// the one already materialized). This skips that work in [`Join::push_child_change`].
459/// The **new** node is still fully materialized (an `Exists` on the forward path
460/// legitimately reads its gated slot).
461pub(crate) fn materialize_change_edit_old_row_only(c: Change) -> OwnedChange {
462    match c {
463        Change::Edit { node, old } => OwnedChange::Edit {
464            node: materialize_node(&node),
465            old: OwnedNode {
466                row: old.row,
467                rels: Vec::new(),
468            },
469        },
470        other => materialize_change(other),
471    }
472}
473
474/// Rebuild a `Node<'g>` from an owned node: each relationship becomes a thunk that
475/// yields its owned children (cloned per call). Owned data is `'static`, so the
476/// rebuilt thunk is valid for any `'g`.
477pub(crate) fn rebuild_node<'g>(on: OwnedNode) -> Node<'g> {
478    let rels = on
479        .rels
480        .into_iter()
481        .map(|(slot, children)| Relationship {
482            slot,
483            thunk: Box::new(move || {
484                let children = children.clone();
485                Box::new(children.into_iter().map(rebuild_node)) as NodeStream<'g>
486            }),
487        })
488        .collect();
489    Node { row: on.row, rels }
490}
491
492/// Rebuild a forwardable `Change<'g>` from an owned change.
493pub(crate) fn rebuild_change<'g>(oc: OwnedChange) -> Change<'g> {
494    match oc {
495        OwnedChange::Add(n) => Change::Add(rebuild_node(n)),
496        OwnedChange::Remove(n) => Change::Remove(rebuild_node(n)),
497        OwnedChange::Edit { node, old } => Change::Edit {
498            node: rebuild_node(node),
499            old: rebuild_node(old),
500        },
501        OwnedChange::Child { node, rel, sub } => Change::Child {
502            node: rebuild_node(node),
503            rel,
504            child: Box::new(rebuild_change(*sub)),
505        },
506    }
507}
508
509/// Marker for which join port a change arrived on (replaces JS's two distinct
510/// `setOutput({push: ...})` closures).
511#[derive(Clone, Copy, Debug)]
512pub enum Port {
513    /// The sole downstream edge of a single-output operator.
514    Single,
515    /// Arrived via the join's parent input.
516    JoinParent,
517    /// Arrived via the join's child input.
518    JoinChild,
519}
520
521/// A downstream edge: who to push to, and on which of their input ports.
522#[derive(Clone, Copy)]
523pub struct OutEdge {
524    pub node: crate::graph::NodeId,
525    pub port: Port,
526}
527
528/// Silence "unused" for fields kept for fidelity/clarity.
529#[allow(dead_code)]
530fn _doc_link(_: &Graph) {}
531
532#[cfg(test)]
533mod tests {
534    use super::*;
535    use crate::value::OwnedValue as V;
536
537    #[test]
538    fn constraints_compatible_on_shared_columns() {
539        // Disjoint columns: always compatible (neither constrains the other).
540        let a: Constraint = vec![(0, V::Int(1))];
541        let b: Constraint = vec![(1, V::Int(2))];
542        assert!(constraints_are_compatible(&a, &b));
543
544        // Shared column, equal value: compatible.
545        let a: Constraint = vec![(0, V::Int(1)), (1, V::Int(2))];
546        let b: Constraint = vec![(0, V::Int(1))];
547        assert!(constraints_are_compatible(&a, &b));
548        assert!(constraints_are_compatible(&b, &a)); // symmetric on the shared set
549
550        // Shared column, conflicting value: incompatible.
551        let b: Constraint = vec![(0, V::Int(9))];
552        assert!(!constraints_are_compatible(&a, &b));
553
554        // Empty constraint imposes nothing.
555        let empty: Constraint = Vec::new();
556        assert!(constraints_are_compatible(&a, &empty));
557        assert!(constraints_are_compatible(&empty, &a));
558    }
559}