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}