triblespace_core/query.rs
1//! Query facilities for matching tribles by declaring patterns of constraints.
2//! Build queries with the [`find!`](crate::prelude::find) macro which binds variables and
3//! combines constraint expressions:
4//!
5//! ```
6//! # use triblespace_core::prelude::*;
7//! # use triblespace_core::prelude::inlineencodings::ShortString;
8//! let results = find!((x: Inline<ShortString>), x.is("foo".to_inline())).collect::<Vec<_>>();
9//! ```
10//!
11//! Variables are converted via [`TryFromInline`](crate::inline::TryFromInline). By default,
12//! conversion failures silently skip the row (filter semantics). Append `?` to a variable
13//! to receive `Result<T, E>` instead, letting the caller handle errors explicitly.
14//!
15//! For a tour of the language see the "Query Language" chapter in the book.
16//! Conceptual background on schemas and join strategy appears in the
17//! "Query Engine" and "Atreides Join" chapters.
18/// [`ConstantConstraint`] — pins a variable to a single value.
19pub mod constantconstraint;
20/// [`EqualityConstraint`](equalityconstraint::EqualityConstraint) — constrains two variables to have the same value.
21pub mod equalityconstraint;
22/// [`latest`](frontier::latest) — the maximal states of an observation DAG.
23pub mod frontier;
24/// Shared finite continuation for immutable, ordered single-variable sources.
25#[doc(hidden)]
26/// [`KeysConstraint`](hashmapconstraint::KeysConstraint) — constrains a variable to HashMap keys.
27pub mod hashmapconstraint;
28/// [`SetConstraint`](hashsetconstraint::SetConstraint) — constrains a variable to HashSet members.
29pub mod hashsetconstraint;
30/// [`IntersectionConstraint`](intersectionconstraint::IntersectionConstraint) — logical AND.
31pub mod intersectionconstraint;
32/// [`ProposalBuffer`] and [`Candidates`] — candidate storage and bit-packed
33/// liveness for one search level.
34mod liveness;
35/// Exact consecutive or explicit row selections.
36mod ordinal;
37/// [`PatchValueConstraint`](patchconstraint::PatchValueConstraint) and [`PatchIdConstraint`](patchconstraint::PatchIdConstraint) — constrains variables to PATCH entries.
38pub mod patchconstraint;
39#[doc(hidden)]
40/// [`InlineRange`](rangeconstraint::InlineRange) — restricts a variable to a byte-lexicographic range.
41pub mod rangeconstraint;
42/// [`resolve`](register::resolve) — registers: states, an order, and the
43/// reader's choice of which are current.
44pub mod register;
45/// [`SortedSliceConstraint`](sortedsliceconstraint::SortedSliceConstraint) — constrains a variable to values in a sorted slice (binary search confirm).
46pub mod sortedsliceconstraint;
47/// [`UnionConstraint`](unionconstraint::UnionConstraint) — logical OR.
48pub mod unionconstraint;
49mod variableset;
50
51use std::cmp::Reverse;
52use std::fmt;
53use std::marker::PhantomData;
54use std::sync::atomic::{AtomicU64, Ordering};
55use std::sync::Arc;
56
57use arrayvec::ArrayVec;
58use constantconstraint::*;
59use ordinal::RowSelection;
60
61use crate::inline::encodings::genid::GenId;
62use crate::inline::Inline;
63use crate::inline::InlineEncoding;
64use crate::inline::RawInline;
65
66/// Re-export of [`VariableSet`](variableset::VariableSet).
67pub use variableset::VariableSet;
68
69/// Re-exports of the candidate/liveness pair. The module they come from is
70/// private on purpose: the bit-packed representation and the region-boundary
71/// invariants that keep it honest are enforced at the boundary of these two
72/// types, so nothing outside `query::liveness` can reach them. Its
73/// module-level comment is the reference for anyone editing it — or writing a
74/// device kernel against [`LIVENESS_WORD_BITS`].
75pub use liveness::{and_words, or_words, Candidates, ProposalBuffer, LIVENESS_WORD_BITS};
76
77impl<T: InlineEncoding> Term<T> {
78 /// Erases the schema type, yielding the runtime representation
79 /// constraint implementations store.
80 pub fn erase(self) -> RawTerm {
81 match self {
82 Term::Var(v) => RawTerm::Var(v.index),
83 Term::Const(c) => RawTerm::Const(c.raw),
84 }
85 }
86}
87
88/// Types storing tribles can implement this trait to expose them to queries.
89/// The trait provides a method to create a constraint for a given trible pattern.
90pub trait TriblePattern {
91 /// The type of the constraint created by the pattern method.
92 ///
93 /// `Send + Sync` is required so the resulting constraint tree can be
94 /// used with the `parallel` feature's rayon iterators. Every in-tree
95 /// pattern backend (TribleSet, SuccinctArchive) satisfies this; custom
96 /// implementations should hold their data behind `Arc` or similar.
97 type PatternConstraint<'a>: Constraint<'a> + Send + Sync
98 where
99 Self: 'a;
100
101 /// Create a constraint for a given trible pattern.
102 /// Each position takes a [`Term`]: either a [`Variable`] to solve for
103 /// or a constant [`Inline`] value baked into the constraint (a constant
104 /// position behaves exactly like a variable the engine has already
105 /// bound, but never appears in the constraint's [`VariableSet`]).
106 /// The schemas of the entities and attributes are always [GenId], while the value
107 /// schema can be any type implementing [InlineEncoding] and is specified as a type parameter.
108 ///
109 /// This method is usually not called directly, but rather through typed query language
110 /// macros like [pattern!][crate::macros::pattern], which pass attribute
111 /// constants and literal values as constant terms.
112 fn pattern<'a, V: InlineEncoding>(
113 &'a self,
114 e: impl Into<Term<GenId>>,
115 a: impl Into<Term<GenId>>,
116 v: impl Into<Term<V>>,
117 ) -> Self::PatternConstraint<'a>;
118}
119
120/// Low-level identifier for a variable in a query.
121pub type VariableId = usize;
122
123/// Context for creating variables in a query.
124/// The context keeps track of the next index to assign to a variable.
125/// This allows for the creation of new anonymous variables in higher-level query languages.
126#[derive(Debug)]
127pub struct VariableContext {
128 /// The index that will be assigned to the next variable.
129 pub next_index: VariableId,
130}
131
132impl Default for VariableContext {
133 fn default() -> Self {
134 Self::new()
135 }
136}
137
138impl VariableContext {
139 /// Create a new variable context.
140 /// The context starts with an index of 0.
141 pub fn new() -> Self {
142 VariableContext { next_index: 0 }
143 }
144
145 /// Create a new variable.
146 /// The variable is assigned the next available index.
147 ///
148 /// Panics if the number of variables exceeds 128.
149 ///
150 /// This method is usually not called directly, but rather through typed query language
151 /// macros like [find!][crate::query].
152 pub fn next_variable<T: InlineEncoding>(&mut self) -> Variable<T> {
153 assert!(
154 self.next_index < 128,
155 "currently queries support at most 128 variables"
156 );
157 let v = Variable::new(self.next_index);
158 self.next_index += 1;
159 v
160 }
161}
162
163/// A placeholder for unknowns in a query.
164/// Within the query engine each variable is identified by an integer,
165/// which can be accessed via the `index` property.
166/// Variables also have an associated type which is used to parse the [Inline]s
167/// found by the query engine.
168#[derive(Debug)]
169pub struct Variable<T: InlineEncoding> {
170 /// The integer index identifying this variable in the [`Binding`].
171 pub index: VariableId,
172 typed: PhantomData<T>,
173}
174
175impl<T: InlineEncoding> Copy for Variable<T> {}
176
177impl<T: InlineEncoding> Clone for Variable<T> {
178 fn clone(&self) -> Self {
179 *self
180 }
181}
182
183impl<T: InlineEncoding> Variable<T> {
184 /// Creates a variable with the given index.
185 pub fn new(index: VariableId) -> Self {
186 Variable {
187 index,
188 typed: PhantomData,
189 }
190 }
191
192 /// Extracts the bound value for this variable from `binding`.
193 ///
194 /// # Panics
195 ///
196 /// Panics if the variable has not been bound.
197 pub fn extract<'b>(self, binding: &Binding<'b>) -> &'b Inline<T> {
198 let raw = binding.get(self.index).unwrap_or_else(|| {
199 panic!(
200 "query variable (idx {}) was never bound before projection. This usually means the variable was projected in `find!` but never appeared in any constraint. If you intended a pure existence query, use `find!((), ...)` or `exists!(constraint)`.",
201 self.index
202 )
203 });
204 Inline::as_transmute_raw(raw)
205 }
206}
207
208/// One position of a triple pattern: either a [`Variable`] the engine
209/// solves for, or a constant [`Inline`] value pinned at construction.
210///
211/// Constants are how the macro layer expresses attribute constants and
212/// literal values without allocating hidden helper variables. A constant
213/// position behaves exactly like a variable that is already bound — the
214/// backends' bound/unbound dispatch handles it with no extra cases — but
215/// it never appears in the constraint's [`VariableSet`]. This keeps the
216/// visible variable set of a `pattern!` equal to the query variables the
217/// user actually wrote, which is what makes
218/// [`or!`](crate::or) over patterns with different attributes or literals
219/// well-formed (all arms declare the same set).
220#[derive(Debug)]
221pub enum Term<T: InlineEncoding> {
222 /// A variable to solve for.
223 Var(Variable<T>),
224 /// A constant value pinned at construction.
225 Const(Inline<T>),
226}
227
228impl<T: InlineEncoding> Copy for Term<T> {}
229
230impl<T: InlineEncoding> Clone for Term<T> {
231 fn clone(&self) -> Self {
232 *self
233 }
234}
235
236impl<T: InlineEncoding> From<Variable<T>> for Term<T> {
237 fn from(v: Variable<T>) -> Self {
238 Term::Var(v)
239 }
240}
241
242impl<T: InlineEncoding> From<Inline<T>> for Term<T> {
243 fn from(value: Inline<T>) -> Self {
244 Term::Const(value)
245 }
246}
247
248/// Untyped runtime form of a [`Term`]: a variable slot index or a pinned
249/// 32-byte value. Constraint implementations store this and use
250/// [`is_var`](RawTerm::is_var) / [`bound`](RawTerm::bound) in place of the
251/// raw `VariableId` comparison and `Binding::get` lookup — a constant term
252/// then flows through the existing bound-position dispatch for free.
253#[derive(Clone, Copy, Debug, Eq, PartialEq)]
254pub enum RawTerm {
255 /// A variable slot index.
256 Var(VariableId),
257 /// A pinned raw value.
258 Const(RawInline),
259}
260
261impl RawTerm {
262 /// Returns `true` when this term is the given variable.
263 #[inline]
264 pub fn is_var(&self, variable: VariableId) -> bool {
265 matches!(self, RawTerm::Var(v) if *v == variable)
266 }
267
268 /// Returns the value this position holds under `binding`: the pinned
269 /// value for a constant, the binding's value (if any) for a variable.
270 ///
271 /// This is the helper that unifies "bound variable" and "constant" —
272 /// backend dispatch keyed on "is this position bound?" treats a
273 /// constant as a position that is born bound, with no extra match arms.
274 #[inline]
275 pub fn position_value<'b>(&'b self, binding: &'b Binding) -> Option<&'b RawInline> {
276 match self {
277 RawTerm::Var(v) => binding.get(*v),
278 RawTerm::Const(c) => Some(c),
279 }
280 }
281
282 /// Adds the term's variable (if it is one) to `set`. Constants stay
283 /// below the variable layer and are never added.
284 #[inline]
285 pub fn add_to(&self, set: &mut VariableSet) {
286 if let RawTerm::Var(v) = self {
287 set.set(*v);
288 }
289 }
290}
291
292/// Collections can implement this trait so that they can be used in queries.
293/// The returned constraint will filter the values assigned to the variable
294/// to only those that are contained in the collection.
295pub trait ContainsConstraint<'a, T: InlineEncoding> {
296 /// The concrete constraint type produced by [`has`](ContainsConstraint::has).
297 type Constraint: Constraint<'a>;
298
299 /// Create a constraint that filters the values assigned to the variable
300 /// to only those that are contained in the collection.
301 ///
302 /// The returned constraint will usually perform a conversion between the
303 /// concrete rust type stored in the collection a [Inline] of the appropriate schema
304 /// type for the variable.
305 fn has(self, v: Variable<T>) -> Self::Constraint;
306}
307
308impl<T: InlineEncoding> Variable<T> {
309 /// Create a constraint so that only a specific value can be assigned to the variable.
310 pub fn is(self, constant: Inline<T>) -> ConstantConstraint {
311 ConstantConstraint::new(self, constant)
312 }
313}
314
315/// The values assigned to the variables of a query — stored as **paths,
316/// not copies**.
317///
318/// A bound variable's value always *originates* from that variable's own
319/// level buffer: [`propose`](Constraint::propose) fills the buffer and
320/// the engine binds by consuming one of its entries. So a binding does
321/// not need to carry the 32 bytes; it carries the `u32` index of the
322/// chosen entry and resolves it through the buffers on read. Two
323/// properties make the index as good as the value:
324///
325/// * A level's buffer is cleared and refilled only when its variable is
326/// (re-)pushed, and the engine only ever pushes a variable that is
327/// currently unbound (deeper levels are unset by backtracking before
328/// their level is re-pushed). While a variable is bound, its buffer is
329/// stable, so its index stays valid for exactly the lifetime of the
330/// binding. Nothing ever appends to a level while its variable is
331/// bound, so the stability is unconditional (see
332/// [`BindingStore::refill`]).
333/// * Buffers are write-once: confirmers kill entries by clearing a
334/// parallel liveness word, and nothing ever moves or rewrites a stored
335/// value once the engine can see it.
336///
337/// `Binding` is therefore a *view* — the index row plus a borrow of the
338/// buffers it indexes into — constructed for the duration of one
339/// constraint call. [`BindingStore`] owns the buffers; the index rows of
340/// a whole batch live in a [`Frontier`].
341///
342/// The payoff is size: an assignment is one `u32` per variable slot
343/// instead of a 32-byte raw value each, a bind is a 4-byte write instead
344/// of a 32-byte copy, cloning the search state no longer memcpies the
345/// values, and a *batch* of bindings is a small integer matrix over
346/// shared buffers rather than a pile of value copies.
347#[derive(Clone, Copy)]
348pub struct Binding<'a> {
349 /// Bitset tracking which variables have been assigned a value.
350 pub bound: VariableSet,
351 indexes: &'a [u32],
352 levels: &'a [LevelValues],
353}
354
355impl fmt::Debug for Binding<'_> {
356 /// Prints the bound variables and the values they resolve to — the
357 /// assignment, not the buffers it is a path through.
358 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
359 f.debug_map()
360 .entries(self.bound.into_iter().map(|v| (v, self.get(v))))
361 .finish()
362 }
363}
364
365impl<'a> Binding<'a> {
366 /// Returns the value bound to `variable`, or `None` when it is
367 /// unbound — resolved through `variable`'s level buffer.
368 pub fn get(&self, variable: VariableId) -> Option<&'a RawInline> {
369 if self.bound.is_set(variable) {
370 Some(&self.levels[variable].buffer()[self.indexes[variable] as usize])
371 } else {
372 None
373 }
374 }
375}
376
377/// Backing for the empty [`Binding`]: nothing is bound, so nothing ever
378/// resolves through them, but a view needs something to point at.
379static NO_LEVELS: [LevelValues; 128] = [const { LevelValues::empty() }; 128];
380static NO_INDEXES: [u32; 128] = [0; 128];
381
382impl Default for Binding<'_> {
383 /// The empty binding — no variable is bound.
384 fn default() -> Self {
385 Binding {
386 bound: VariableSet::new_empty(),
387 indexes: &NO_INDEXES,
388 levels: &NO_LEVELS,
389 }
390 }
391}
392
393/// A **batch of parent bindings** — the collection [`Constraint::propose`]
394/// expands and [`Constraint::confirm`] filters against.
395///
396/// Because bindings are indexes rather than values, a frontier is an
397/// *index matrix*, not a pile of copied assignments: one row of `stride`
398/// `u32`s per parent binding, over the level buffers those indexes point
399/// into. Identity selections and their offset slices remain consecutive and
400/// allocation-free, while a genuine permutation borrows explicit row
401/// numbers. Narrowing never copies an index row.
402///
403/// Every row of a frontier has the *same* [`bound`](Frontier::bound) set —
404/// they are all at the same point in the search, differing only in which
405/// values they took. Today's single-binding call sites are a frontier of
406/// one; see [`BindingStore::frontier`].
407#[derive(Clone, Copy)]
408pub struct Frontier<'a> {
409 bound: VariableSet,
410 block: &'a [u32],
411 stride: usize,
412 select: RowSelection<'a>,
413 levels: &'a [LevelValues],
414 /// Whether this frontier is being driven by `QueryParIter`.
415 ///
416 /// Crate-private execution intent lets built-in CPU constraints expose
417 /// work to the same Rayon pool without making ordinary `Iterator`
418 /// queries parallel merely because the crate was built with the
419 /// `parallel` feature.
420 parallel: bool,
421}
422
423impl fmt::Debug for Frontier<'_> {
424 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
425 f.debug_list().entries(self.rows()).finish()
426 }
427}
428
429impl Default for Frontier<'_> {
430 /// A batch of exactly one empty binding — the root of every search,
431 /// and the shape a caller with nothing bound hands a constraint.
432 fn default() -> Self {
433 Frontier {
434 bound: VariableSet::new_empty(),
435 block: &NO_INDEXES,
436 stride: NO_INDEXES.len(),
437 select: RowSelection::identity(1),
438 levels: &NO_LEVELS,
439 parallel: false,
440 }
441 }
442}
443
444impl<'a> Frontier<'a> {
445 /// Number of parent bindings in this batch.
446 pub fn len(&self) -> usize {
447 self.select.len()
448 }
449
450 /// True when the batch is empty.
451 pub fn is_empty(&self) -> bool {
452 self.select.len() == 0
453 }
454
455 /// The variables bound in *every* row of this batch.
456 pub fn bound(&self) -> VariableSet {
457 self.bound
458 }
459
460 /// Whether this batch belongs to a query explicitly converted with
461 /// `into_par_iter`.
462 #[cfg(feature = "parallel")]
463 pub(crate) fn parallel(&self) -> bool {
464 self.parallel
465 }
466
467 /// The binding of row `i`.
468 pub fn row(&self, i: usize) -> Binding<'a> {
469 Binding {
470 bound: self.bound,
471 indexes: self.row_indexes(i),
472 levels: self.levels,
473 }
474 }
475
476 /// Iterates the batch's bindings in row order.
477 pub fn rows(&self) -> impl Iterator<Item = Binding<'a>> + '_ {
478 (0..self.len()).map(|i| self.row(i))
479 }
480
481 fn row_indexes(&self, i: usize) -> &'a [u32] {
482 let row = self.select.get(i).expect("frontier row index is in bounds") as usize;
483 &self.block[row * self.stride..(row + 1) * self.stride]
484 }
485
486 /// Translates `positions` (row numbers *in this view*) into row
487 /// numbers of the underlying block, appending them to `out`.
488 ///
489 /// The result is the `select` array of a sub-batch: pair it with
490 /// [`with_select`](Frontier::with_select) to hand a subset of this
491 /// frontier to a child constraint without copying any row.
492 pub fn compose(&self, positions: impl IntoIterator<Item = u32>, out: &mut Vec<u32>) {
493 out.extend(positions.into_iter().map(|position| {
494 self.select
495 .get(position as usize)
496 .expect("frontier composition index is in bounds")
497 }));
498 }
499
500 /// This frontier restricted to `select` — row numbers of the
501 /// underlying block, as produced by [`compose`](Frontier::compose).
502 pub fn with_select<'b>(&self, select: &'b [u32]) -> Frontier<'b>
503 where
504 'a: 'b,
505 {
506 Frontier {
507 bound: self.bound,
508 block: self.block,
509 stride: self.stride,
510 select: RowSelection::explicit(select),
511 levels: self.levels,
512 parallel: self.parallel,
513 }
514 }
515}
516
517/// Owns what a [`Binding`] is a view of: the per-variable level buffers,
518/// plus the index row that picks one entry out of each for callers
519/// outside the search.
520///
521/// The query engine keeps its frontiers' index matrices separately (they
522/// are per search *depth*, while buffers are per *variable*) and reaches
523/// the buffers through [`refill`](BindingStore::refill) and
524/// [`take_chunk`](BindingStore::take_chunk). Code outside the engine
525/// builds a binding over values it picked itself — see
526/// [`bind`](BindingStore::bind) — and turns it into a width-1
527/// [`Frontier`] with [`frontier`](BindingStore::frontier).
528#[derive(Clone)]
529pub struct BindingStore {
530 bound: VariableSet,
531 indexes: [u32; 128],
532 levels: [LevelValues; 128],
533}
534
535impl fmt::Debug for BindingStore {
536 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
537 fmt::Debug::fmt(&self.view(), f)
538 }
539}
540
541impl Default for BindingStore {
542 fn default() -> Self {
543 BindingStore {
544 bound: VariableSet::new_empty(),
545 indexes: [0; 128],
546 levels: std::array::from_fn(|_| LevelValues::default()),
547 }
548 }
549}
550
551impl BindingStore {
552 /// An empty store: nothing bound, every level empty.
553 pub fn new() -> Self {
554 Self::default()
555 }
556
557 /// The current assignment, as [`satisfied`](Constraint::satisfied)
558 /// and result post-processing see it.
559 pub fn view(&self) -> Binding<'_> {
560 Binding {
561 bound: self.bound,
562 indexes: &self.indexes,
563 levels: &self.levels,
564 }
565 }
566
567 /// The current assignment as a **frontier of one** — the batch shape
568 /// [`propose`](Constraint::propose) and [`confirm`](Constraint::confirm)
569 /// take. This is the bridge for every caller that has a single
570 /// binding: a collection of one behaves exactly like the old
571 /// single-binding protocol.
572 pub fn frontier(&self) -> Frontier<'_> {
573 Frontier {
574 bound: self.bound,
575 block: &self.indexes,
576 stride: self.indexes.len(),
577 select: RowSelection::identity(1),
578 levels: &self.levels,
579 parallel: false,
580 }
581 }
582
583 /// Binds `variable` to `value` by appending it to that variable's
584 /// level buffer and pointing the index row at it.
585 ///
586 /// The engine never needs this — its candidates are already in the
587 /// buffer, so it binds by index. This is the entry point for callers
588 /// outside the search (tests, tools) that want a binding over values
589 /// of their own choosing.
590 pub fn bind(&mut self, variable: VariableId, value: &RawInline) {
591 let level = &mut self.levels[variable];
592 let buffer = Arc::make_mut(
593 level
594 .buffer
595 .get_or_insert_with(|| Arc::new(ProposalBuffer::new())),
596 );
597 let index = buffer.len();
598 buffer.push(*value);
599 self.indexes[variable] = index as u32;
600 self.bound.set(variable);
601 }
602
603 /// Unbinds `variable` — the engine's backtracking step.
604 pub fn unset(&mut self, variable: VariableId) {
605 self.bound.unset(variable);
606 }
607
608 /// The set of currently bound variables.
609 pub fn bound(&self) -> VariableSet {
610 self.bound
611 }
612
613 /// Clears `variable`'s level and refills it by proposing over the
614 /// frontier `block`/`select`/`stride` describe.
615 ///
616 /// The level is moved *out* of the array for the duration of the
617 /// call, so `propose` gets `&mut` on it while the remaining levels
618 /// lend immutably through the [`Frontier`] view — the borrow split
619 /// the search needs, with no `unsafe` and no second copy of the
620 /// candidates. This is sound precisely because the engine only ever
621 /// refills the level of a variable it is about to push, which is by
622 /// construction unbound: nothing can be resolving through this level
623 /// while it is away.
624 ///
625 /// This is now the *only* way a level's buffer is written. With the
626 /// resumable-narrowing path gone, no level is ever appended to while
627 /// its variable is bound, so "a bound variable's buffer is stable for
628 /// the lifetime of its binding" holds unconditionally — the
629 /// `debug_assert` below is that invariant.
630 ///
631 /// A uniquely owned buffer is cleared in place so ordinary backtracking
632 /// retains its capacity. If a rayon sibling shares the published buffer,
633 /// refill replaces this branch's handle with a fresh empty buffer before
634 /// calling `propose`; cloning and then clearing the old contents would be
635 /// both slower and semantically unnecessary.
636 pub(crate) fn refill(
637 &mut self,
638 variable: VariableId,
639 block: &[u32],
640 select: RowSelection<'_>,
641 stride: usize,
642 parallel: bool,
643 propose: impl FnOnce(&Frontier<'_>, &mut ProposalBuffer),
644 ) -> usize {
645 debug_assert!(
646 !self.bound.is_set(variable),
647 "refilling a bound variable's level would strand its binding"
648 );
649 let mut level = std::mem::take(&mut self.levels[variable]);
650 let mut buffer = level
651 .buffer
652 .take()
653 .unwrap_or_else(|| Arc::new(ProposalBuffer::new()));
654 if Arc::get_mut(&mut buffer).is_none() {
655 // A rayon sibling still resolves indexes through the published
656 // buffer. Refill discards every old entry, so cloning it before
657 // `clear` would be pure waste: replace our handle with a fresh
658 // empty allocation and leave the sibling's snapshot untouched.
659 buffer = Arc::new(ProposalBuffer::new());
660 }
661 level.pos = 0;
662 let proposed = {
663 let buffer = Arc::get_mut(&mut buffer).expect("sole owner after replacement");
664 buffer.clear();
665 propose(
666 &Frontier {
667 bound: self.bound,
668 block,
669 stride,
670 select,
671 levels: &self.levels,
672 parallel,
673 },
674 buffer,
675 );
676 buffer.len()
677 };
678 level.buffer = Some(buffer);
679 self.levels[variable] = level;
680 proposed
681 }
682
683 /// A [`Frontier`] over the rows `block`/`select`/`stride` describe,
684 /// resolving through this store's level buffers.
685 pub(crate) fn batch<'s>(
686 &'s self,
687 block: &'s [u32],
688 select: RowSelection<'s>,
689 stride: usize,
690 parallel: bool,
691 ) -> Frontier<'s> {
692 Frontier {
693 bound: self.bound,
694 block,
695 stride,
696 select,
697 levels: &self.levels,
698 parallel,
699 }
700 }
701
702 /// How far `variable`'s level cursor has run — the count of entries the
703 /// level has consumed or skipped. With `Level::proposed` this bounds the
704 /// candidates still pending without scanning the liveness words.
705 pub(crate) fn consumed(&self, variable: VariableId) -> usize {
706 self.levels[variable].pos
707 }
708
709 /// Consumes up to `width` further live candidates from `variable`'s
710 /// level and writes the child frontier's rows into `out`, recording
711 /// each child row's parent row number in `parents_out`.
712 ///
713 /// Each consumed candidate `i` carries the parent tag its proposer
714 /// wrote; the child row is that parent's index row with `variable`'s
715 /// slot pointed at `i`. Returns how many rows were produced (zero
716 /// means the level is spent).
717 ///
718 /// This is the fused path: one pass, no intermediate. A descent that
719 /// might be 1:1 uses [`draw`](BindingStore::draw) instead, which defers
720 /// the write until the shape of the draw is known.
721 pub(crate) fn take_chunk(
722 &mut self,
723 variable: VariableId,
724 width: usize,
725 parent_block: &[u32],
726 parent_select: RowSelection<'_>,
727 stride: usize,
728 out: &mut Vec<u32>,
729 parents_out: &mut Vec<u32>,
730 ) -> usize {
731 out.clear();
732 parents_out.clear();
733 let level = &mut self.levels[variable];
734 let buffer = level
735 .buffer
736 .as_deref()
737 .expect("an active level must have a proposal buffer");
738 let pos = &mut level.pos;
739 let mut rows = 0;
740 while rows < width {
741 let Some(i) = buffer.next_live(*pos) else {
742 break;
743 };
744 *pos = i + 1;
745 let parent = parent_select
746 .get(buffer.parent_of(i) as usize)
747 .expect("candidate parent tag is in bounds") as usize;
748 out.extend_from_slice(&parent_block[parent * stride..(parent + 1) * stride]);
749 let base = out.len() - stride;
750 out[base + variable] = i as u32;
751 parents_out.push(parent as u32);
752 rows += 1;
753 }
754 self.bound.set_value(variable, rows != 0);
755 rows
756 }
757
758 /// Consumes up to `width` further live candidates from `variable`'s
759 /// level *without* materialising any child row: each drawn entry's
760 /// index lands in `drawn_out` and the frontier row it was proposed for
761 /// in `parents_out`.
762 ///
763 /// Deferring the write is what lets the engine look at the shape of a
764 /// draw before paying for it — a 1:1 draw needs no new rows at all,
765 /// see [`Query::next_chunk`]. It is not free (two passes instead of
766 /// one), which is exactly why the caller only takes this path when a
767 /// 1:1 draw is *possible*.
768 pub(crate) fn draw(
769 &mut self,
770 variable: VariableId,
771 width: usize,
772 parent_select: RowSelection<'_>,
773 drawn_out: &mut Vec<u32>,
774 parents_out: &mut Vec<u32>,
775 ) {
776 drawn_out.clear();
777 parents_out.clear();
778 let level = &mut self.levels[variable];
779 let buffer = level
780 .buffer
781 .as_deref()
782 .expect("an active level must have a proposal buffer");
783 let pos = &mut level.pos;
784 while drawn_out.len() < width {
785 let Some(i) = buffer.next_live(*pos) else {
786 break;
787 };
788 *pos = i + 1;
789 drawn_out.push(i as u32);
790 parents_out.push(
791 parent_select
792 .get(buffer.parent_of(i) as usize)
793 .expect("candidate parent tag is in bounds"),
794 );
795 }
796 self.bound.set_value(variable, !drawn_out.is_empty());
797 }
798
799 /// Whether `variable`'s level has no live candidate left beyond what
800 /// has already been drawn.
801 ///
802 /// Deliberately *not* folded into [`draw`](BindingStore::draw): it is a
803 /// forward scan over the liveness words, so asking after every draw
804 /// would pay a second pass over a level's dead tail that the next draw
805 /// pays anyway. The one caller asks only once the cheap `O(1)`
806 /// conditions for an in-place descent already hold.
807 pub(crate) fn spent(&self, variable: VariableId) -> bool {
808 let level = &self.levels[variable];
809 level.buffer().next_live(level.pos).is_none()
810 }
811}
812
813/// The cooperative protocol that every query participant implements.
814///
815/// A constraint restricts the values that can be assigned to query variables.
816/// The query engine does not plan joins in advance; instead it consults
817/// constraints directly during a depth-first search over partial bindings.
818/// Each constraint reports which variables it touches, estimates how many
819/// candidates remain, enumerates concrete values on demand, and signals
820/// whether its requirements are still satisfiable. This protocol is the
821/// sole interface between the engine and the data — whether that data lives
822/// in a [`TribleSet`](crate::trible::TribleSet), a [`HashMap`](std::collections::HashMap),
823/// or a custom application predicate.
824///
825/// # The protocol
826///
827/// The engine drives the search by calling five methods in a fixed rhythm:
828///
829/// | Method | Role | Called when |
830/// |--------|------|------------|
831/// | [`variables`](Constraint::variables) | Declares which variables the constraint touches. | Once, at query start. |
832/// | [`estimate`](Constraint::estimate) | Predicts the candidate count for a variable. | Before each binding decision. |
833/// | [`propose`](Constraint::propose) | Enumerates candidate values for a variable, for a whole batch of bindings. | On the most selective constraint. |
834/// | [`confirm`](Constraint::confirm) | Filters candidates proposed by another constraint. | On all remaining constraints. |
835/// | [`satisfied`](Constraint::satisfied) | Checks whether fully-bound sub-constraints still hold. | Before propose/confirm in composite constraints. |
836///
837/// [`influence`](Constraint::influence) completes the picture by telling the
838/// engine which estimates to refresh when a variable is bound or unbound.
839///
840/// # Batching
841///
842/// `propose` and `confirm` operate on a **[`Frontier`]** — a collection of
843/// parent bindings — rather than on one binding. Today's single-binding
844/// call sites are a frontier of one ([`BindingStore::frontier`]) and behave
845/// exactly as before; the engine drives wider batches so that every level
846/// of the search offers a source a *wide* region to work on, not just the
847/// root. A source that has nothing to gain from width writes the loop over
848/// [`Frontier::rows`] and is done; one that does (a GPU-resident index, a
849/// SIMD probe) sees the whole batch in one call. Nothing in the protocol
850/// requires a source to seek: iteration and point queries still suffice,
851/// which is what keeps hash maps, PATCHes, succinct archives and
852/// device-resident structures all admissible.
853///
854/// # Statelessness
855///
856/// Constraints are stateless: every method receives the current [`Binding`]
857/// as a parameter rather than maintaining internal bookkeeping. This lets
858/// the engine backtrack freely by unsetting variables in the binding
859/// without notifying the constraints.
860///
861/// # Composability
862///
863/// Constraints combine via [`IntersectionConstraint`](crate::query::intersectionconstraint::IntersectionConstraint)
864/// (logical AND — built by [`and!`](crate::and)) and
865/// [`UnionConstraint`](crate::query::unionconstraint::UnionConstraint)
866/// (logical OR — built by [`or!`](crate::or)). Because every constraint
867/// speaks the same protocol, heterogeneous data sources mix freely in a
868/// single query.
869///
870/// # Implementing a custom constraint
871///
872/// A new constraint only needs to implement [`variables`](Constraint::variables),
873/// [`estimate`](Constraint::estimate), [`propose`](Constraint::propose), and
874/// [`confirm`](Constraint::confirm). Override [`satisfied`](Constraint::satisfied)
875/// when the constraint can detect unsatisfiability before the engine asks
876/// about individual variables (e.g. a fully-bound triple lookup that found
877/// no match). Override [`influence`](Constraint::influence) when binding one
878/// variable changes the estimates for a non-obvious set of others.
879pub trait Constraint<'a> {
880 /// Returns the set of variables this constraint touches.
881 ///
882 /// Called once at query start. The engine uses this to build influence
883 /// graphs and to determine which constraints participate when a
884 /// particular variable is being bound.
885 fn variables(&self) -> VariableSet;
886
887 /// Estimates the number of candidate values for `variable` given the
888 /// current partial `binding`.
889 ///
890 /// Returns `None` when `variable` is not constrained by this constraint.
891 /// The estimate need not be exact — it guides variable ordering, not
892 /// correctness. Tighter estimates lead to better search pruning; see the
893 /// [Atreides join](crate) family for how different estimate fidelities
894 /// affect performance.
895 ///
896 /// Whether the answer is `Some` or `None` must depend only on which
897 /// variables are bound, not on the values they are bound to: every row
898 /// of a [`Frontier`] shares one bound set, and composites read
899 /// relevance off the batch.
900 fn estimate(&self, variable: VariableId, binding: &Binding) -> Option<usize>;
901
902 /// Enumerates candidate values for `variable`, for every row of
903 /// `frontier`, into `proposals`.
904 ///
905 /// Called on the constraint with the lowest estimate for the variable
906 /// being bound. Before appending the candidates of row `r`, the
907 /// proposer calls [`ProposalBuffer::open`] with `r`, which tags them so
908 /// confirmers and the engine can tell whose they are. A proposer may
909 /// visit the rows in any order and interleave them; contiguous segments
910 /// are merely the cheaper shape.
911 ///
912 /// Values are appended to `proposals`; entries appended by an enclosing
913 /// composite's other children may precede them, and must be left
914 /// untouched.
915 ///
916 /// Does nothing when `variable` is not constrained by this constraint.
917 fn propose(
918 &self,
919 variable: VariableId,
920 frontier: &Frontier<'_>,
921 proposals: &mut ProposalBuffer,
922 );
923
924 /// Kills [`Candidates`] entries whose values for `variable` violate
925 /// this constraint under the entry's own frontier row.
926 ///
927 /// Called on every constraint *except* the one that proposed, in order
928 /// of increasing estimate, all killing into the same region —
929 /// sequential kills compute the conjunction. Implementations may only
930 /// kill entries, never revive them, and may skip entries that are
931 /// already dead. Nothing is ever compacted: dead entries stay in place
932 /// and the engine iterates live ones.
933 ///
934 /// The region spans the whole batch: entry `i` belongs to frontier row
935 /// [`cands.parent(i)`](Candidates::parent). A confirmer whose verdict
936 /// depends on the parent binding walks the region with
937 /// [`Candidates::for_each_parent`]; one whose verdict does not simply
938 /// ignores the tags.
939 ///
940 /// Does nothing when `variable` is not constrained by this constraint.
941 fn confirm(&self, variable: VariableId, frontier: &Frontier<'_>, cands: &mut Candidates<'_>);
942
943 /// Returns whether this constraint is consistent with the current
944 /// `binding`.
945 ///
946 /// The default implementation returns `true`. Override this when the
947 /// constraint can cheaply detect that no solution exists — for example,
948 /// a `TribleSetConstraint`
949 /// whose entity, attribute, and value are all bound but the triple is
950 /// absent from the dataset.
951 ///
952 /// Composite constraints propagate this check to their children:
953 /// [`IntersectionConstraint`](crate::query::intersectionconstraint::IntersectionConstraint)
954 /// requires *all* children to be satisfied, while
955 /// [`UnionConstraint`](crate::query::unionconstraint::UnionConstraint)
956 /// requires *at least one*. The union uses this to skip dead variants
957 /// in propose and confirm, preventing values from a satisfied variant
958 /// from leaking through a dead one.
959 fn satisfied(&self, _binding: &Binding) -> bool {
960 true
961 }
962
963 /// Returns the set of variables whose estimates may change when
964 /// `variable` is bound or unbound.
965 ///
966 /// The default includes every variable this constraint touches except
967 /// `variable` itself. Returns an empty set when `variable` is not part
968 /// of this constraint.
969 fn influence(&self, variable: VariableId) -> VariableSet {
970 let mut vars = self.variables();
971 if vars.is_set(variable) {
972 vars.unset(variable);
973 vars
974 } else {
975 VariableSet::new_empty()
976 }
977 }
978}
979
980impl<'a, T: Constraint<'a> + ?Sized> Constraint<'a> for Box<T> {
981 fn variables(&self) -> VariableSet {
982 let inner: &T = self;
983 inner.variables()
984 }
985
986 fn estimate(&self, variable: VariableId, binding: &Binding) -> Option<usize> {
987 let inner: &T = self;
988 inner.estimate(variable, binding)
989 }
990
991 fn propose(
992 &self,
993 variable: VariableId,
994 frontier: &Frontier<'_>,
995 proposals: &mut ProposalBuffer,
996 ) {
997 let inner: &T = self;
998 inner.propose(variable, frontier, proposals)
999 }
1000
1001 fn confirm(&self, variable: VariableId, frontier: &Frontier<'_>, cands: &mut Candidates<'_>) {
1002 let inner: &T = self;
1003 inner.confirm(variable, frontier, cands)
1004 }
1005
1006 fn satisfied(&self, binding: &Binding) -> bool {
1007 let inner: &T = self;
1008 inner.satisfied(binding)
1009 }
1010
1011 fn influence(&self, variable: VariableId) -> VariableSet {
1012 let inner: &T = self;
1013 inner.influence(variable)
1014 }
1015}
1016
1017impl<'a, T: Constraint<'a> + ?Sized> Constraint<'a> for std::sync::Arc<T> {
1018 fn variables(&self) -> VariableSet {
1019 let inner: &T = self;
1020 inner.variables()
1021 }
1022
1023 fn estimate(&self, variable: VariableId, binding: &Binding) -> Option<usize> {
1024 let inner: &T = self;
1025 inner.estimate(variable, binding)
1026 }
1027
1028 fn propose(
1029 &self,
1030 variable: VariableId,
1031 frontier: &Frontier<'_>,
1032 proposals: &mut ProposalBuffer,
1033 ) {
1034 let inner: &T = self;
1035 inner.propose(variable, frontier, proposals)
1036 }
1037
1038 fn confirm(&self, variable: VariableId, frontier: &Frontier<'_>, cands: &mut Candidates<'_>) {
1039 let inner: &T = self;
1040 inner.confirm(variable, frontier, cands)
1041 }
1042
1043 fn satisfied(&self, binding: &Binding) -> bool {
1044 let inner: &T = self;
1045 inner.satisfied(binding)
1046 }
1047
1048 fn influence(&self, variable: VariableId) -> VariableSet {
1049 let inner: &T = self;
1050 inner.influence(variable)
1051 }
1052}
1053
1054/// Per-variable candidate storage for one search level: the proposals for
1055/// the whole batch that produced them, plus how far the engine has consumed
1056/// them. Slots are indexed by [`VariableId`], so siblings — different
1057/// variables chosen at the same depth — never share one; what a slot is
1058/// reused for is the *next* binding of its own variable, whose refill keeps
1059/// the buffer's capacity.
1060#[derive(Clone, Debug)]
1061struct LevelValues {
1062 /// Once proposed and confirmed, a buffer is immutable. Rayon siblings
1063 /// therefore share it and keep only their consumption cursor local.
1064 /// `None` is the never-used state, so an empty [`BindingStore`] needs no
1065 /// heap allocation for its 128 variable slots.
1066 buffer: Option<Arc<ProposalBuffer>>,
1067 /// Consumption position: entries before it are consumed, live entries at
1068 /// or after it are pending. Dead entries are skipped, never moved.
1069 pos: usize,
1070}
1071
1072impl LevelValues {
1073 /// An empty level, const-constructible so the empty [`Binding`] can
1074 /// borrow a `static` array of them.
1075 const fn empty() -> Self {
1076 LevelValues {
1077 buffer: None,
1078 pos: 0,
1079 }
1080 }
1081
1082 /// The published, immutable proposals for this level.
1083 fn buffer(&self) -> &ProposalBuffer {
1084 self.buffer
1085 .as_deref()
1086 .expect("a bound or active level must have a proposal buffer")
1087 }
1088}
1089
1090impl Default for LevelValues {
1091 fn default() -> Self {
1092 Self::empty()
1093 }
1094}
1095
1096/// Widest frontier the engine expands at once.
1097///
1098/// The search keeps a *frontier* — all the rows sitting at one point of the
1099/// tree — and expands up to this many of them in a single
1100/// [`propose`](Constraint::propose)/[`confirm`](Constraint::confirm) pass.
1101/// Width is what makes a level's candidate region large: with a width-1
1102/// frontier only the root level ever proposes widely, and every deeper level
1103/// hands a source a handful of candidates for one parent — measured on real
1104/// data, a median region of 1–7 candidates at every scale, which is far
1105/// below any batch-dispatch threshold.
1106///
1107/// 16384 is chosen to equal
1108/// `triblespace_gpu::batch_confirm::DEFAULT_MIN_CONFIRM_BATCH`, the measured
1109/// crossover at which a device round trip beats the CPU probes. At that
1110/// width a level's region carries at least as many candidates as there are
1111/// rows, so the batched tier is reachable at *every* depth rather than only
1112/// at a wide root.
1113///
1114/// It is a *ceiling*, not the first batch: a level's first chunk is
1115/// [`INITIAL_FRONTIER_WIDTH`], then chunk width grows by
1116/// [`FRONTIER_RAMP_BASE`] until it reaches this ceiling. A conservative tail
1117/// merge may consume the remainder early, but never exceeds the ceiling.
1118///
1119/// The frontier matrices cost `O(width · variables · depth)`, but a level's
1120/// proposal buffer can be larger: proposing `fanout` candidates for every row
1121/// in one group materialises `O(width · fanout)` entries before that level is
1122/// consumed. Each entry currently has 32 bytes of value, a four-byte parent
1123/// tag, and one packed liveness bit — nominally 36.125 bytes, before allocator
1124/// capacity slack. [`FrontierStats::peak_region`] observes this high-water mark
1125/// in entries; [`Query::with_frontier_width`] is the direct way to reduce it
1126/// when a query joins against a high-fanout source.
1127///
1128/// Worst-case optimality is untouched: expanding N prefixes together is the
1129/// same total work as expanding them one at a time, and the AGM bound is a
1130/// statement about output size, not traversal order.
1131///
1132/// Tune per query with [`Query::with_frontier_width`].
1133pub const DEFAULT_FRONTIER_WIDTH: usize = 16384;
1134
1135/// Width of the *first* chunk a level draws. Later chunks grow by
1136/// [`FRONTIER_RAMP_BASE`] up to [`DEFAULT_FRONTIER_WIDTH`] (or whatever
1137/// [`Query::with_frontier_width`] set).
1138///
1139/// Starting at one keeps time-to-first-result identical to plain
1140/// depth-first search. A query the caller stops after one row — `exists!`,
1141/// `.next()` — must not pay to materialise a full-width frontier it will
1142/// throw away, and with a first chunk of one it does exactly the work the
1143/// pre-batching engine did. Measured on a first-row-only join, a flat
1144/// full-width engine is **8.8x** slower than the pre-batching engine;
1145/// one narrow chunk closes the whole gap.
1146///
1147/// This is the same insight as the `INITIAL_CHUNK`/`WIDEN_FACTOR` pair this
1148/// branch deleted, and as the deleted residual engine's rule that search
1149/// width grows geometrically after negative work — recovered at the right
1150/// layer (the frontier) instead of the wrong one (per-parent chunking).
1151///
1152/// Starting at one protects `.next()` and `exists!`; the ramp then trades
1153/// low-demand granularity against full-drain batch width. The base controls
1154/// that trade. Doubling was measured and rejected because its final chunk is
1155/// only about half a drain, but base eight retains seven eighths
1156/// asymptotically while reaching the ceiling in far fewer steps. See
1157/// [`FRONTIER_RAMP_BASE`] for the measured rationale.
1158pub const INITIAL_FRONTIER_WIDTH: usize = 1;
1159
1160/// Factor a level's chunk width grows by after each chunk, from
1161/// [`INITIAL_FRONTIER_WIDTH`] up to the query's ceiling.
1162///
1163/// # Why a base, and why not two
1164///
1165/// A ramp's cost is not its number of steps, it is the size of its LAST
1166/// chunk. Ramping by `b` consumes `1 + b + b^2 + … + b^k`, of which the
1167/// final chunk `b^k` is `(b-1)/b` of the total — so the widest frontier a
1168/// level can build is `N - N/b`, and the peak is what batching exists to
1169/// produce. At `b = 2` that is `N/2`: doubling throws away half the peak,
1170/// which is the whole of the measured 2048 -> 512 that got the geometric
1171/// ramp rejected. The failure was the base, not the ramp.
1172///
1173/// At `b = 8` the peak is `7N/8` and a level reaches a 16384 ceiling in
1174/// `log8(16384) ~ 4.7` chunks rather than doubling's fourteen — the
1175/// amortised expansion overhead stops mattering well before the peak does.
1176///
1177/// A base at or above the ceiling reproduces the one-narrow-chunk-then-
1178/// ceiling schedule this replaced, which is the control arm to measure
1179/// against. Base `1` is NOT that control — `width * 1` never grows, so it
1180/// pins the engine to a width-1 frontier and reproduces the pre-batching
1181/// engine instead. Both are useful arms; they are not the same arm.
1182pub const FRONTIER_RAMP_BASE: usize = 8;
1183
1184/// A query is an iterator over the results of a query.
1185/// It takes a constraint and a post-processing function as input,
1186/// and returns the results of the query as a stream of values.
1187/// The query engine uses a depth-first search to find solutions to the query,
1188/// proposing values for the variables and backtracking when it reaches a dead end.
1189/// The query engine is designed to be simple and efficient, providing low, consistent,
1190/// and predictable latency, skew resistance, and no required (or possible) tuning.
1191/// The query engine is designed to be used in combination with the [Constraint] trait,
1192/// which provides a simple and flexible way to implement constraints that can be used
1193/// to filter the results of a query.
1194///
1195/// This struct is usually not created directly, but rather through the `find!` macro,
1196/// which provides a convenient way to declare variables and concrete types for them.
1197/// And which sets up the nessecairy context for higher-level query languages
1198/// like the one provided by the [`crate::macros`] module.
1199pub struct Query<C, P: Fn(&Binding<'_>) -> Option<R>, R> {
1200 constraint: C,
1201 postprocessing: P,
1202 mode: Search,
1203 bindings: BindingStore,
1204 influences: [VariableSet; 128],
1205 unbound: VariableSet,
1206 /// Index-row width: one slot per variable the query mentions.
1207 slots: usize,
1208 width: usize,
1209 /// Set only by `IntoParallelIterator`; carried into every frontier so a
1210 /// built-in leaf may join the same pool without changing ordinary
1211 /// iterator execution.
1212 parallel: bool,
1213 stack: ArrayVec<Level, 128>,
1214 /// Frontier stack. `depths[0]` is the root (one empty row); `depths[d]`
1215 /// holds the rows with `d` variables bound. Entries above `depth` are
1216 /// retired but keep their allocations for the next descent.
1217 depths: Vec<Depth>,
1218 depth: usize,
1219 /// Scratch for per-row preferred variables, materialized only after the
1220 /// first disagreement in an expansion.
1221 choice: Vec<u32>,
1222 /// Scratch: the level-buffer entry index of each freshly-drawn child row.
1223 drawn: Vec<u32>,
1224 /// Scratch: each freshly-drawn child row's parent row number.
1225 parents: Vec<u32>,
1226 stats: Arc<FrontierStats>,
1227}
1228
1229/// One pushed search level: the variable it binds, how wide its *next*
1230/// chunk may be, and how many candidates its proposer produced.
1231///
1232/// The width is per level rather than per query because the schedule is a
1233/// statement about one level's drain: the first chunk a level hands down is
1234/// a single row, so a caller who stops after one result never pays for a
1235/// batch it will not look at. See [`INITIAL_FRONTIER_WIDTH`].
1236///
1237/// `proposed` is the `O(1)` gate on the in-place descent: a level with more
1238/// candidates than parent rows cannot possibly yield one child per parent,
1239/// so the engine never pays to check. See [`Query::next_chunk`].
1240#[derive(Clone, Copy, Debug)]
1241struct Level {
1242 variable: VariableId,
1243 width: usize,
1244 proposed: usize,
1245}
1246
1247/// Empties one of a [`Depth`]'s `Arc`-shared buffers for rewriting, without
1248/// copying the contents it is about to discard.
1249///
1250/// `Arc::make_mut` deep-clones whenever the `Arc` is shared — which is the
1251/// normal state right after a rayon split, since splitting hands both halves
1252/// the same matrices — and every caller here clears the result on the next
1253/// line. That copy is pure waste, and it is proportional to the frontier:
1254/// `rows * slots` entries, so it grows with exactly the width the engine
1255/// exists to make large.
1256///
1257/// When we are the sole owner the allocation is reused. When we are not, a
1258/// fresh empty buffer replaces our handle and the other half keeps the old
1259/// one untouched, which is the same outcome `make_mut` produces minus the
1260/// memcpy.
1261fn reset_shared<T: Clone>(slot: &mut Arc<Vec<T>>, capacity: usize) -> &mut Vec<T> {
1262 if Arc::get_mut(slot).is_none() {
1263 *slot = Arc::new(Vec::with_capacity(capacity));
1264 }
1265 let buffer = Arc::get_mut(slot).expect("sole owner after replacement");
1266 buffer.clear();
1267 buffer.reserve(capacity);
1268 buffer
1269}
1270
1271/// The variable choice shared by every row, or the first exact partition
1272/// observed when rows disagree.
1273///
1274/// `Unplanned` and `Universal` carry no allocation. A split plan owns the
1275/// stable variable groups and stores an order override only when stable
1276/// grouping genuinely permutes row ordinals.
1277#[derive(Clone, Debug, Default)]
1278enum DepthPlan {
1279 #[default]
1280 Unplanned,
1281 Universal(VariableId),
1282 Split(Arc<SplitPlan>),
1283}
1284
1285/// Explicit metadata for a frontier whose rows choose different variables.
1286#[derive(Debug, Default)]
1287struct SplitPlan {
1288 /// Stable grouped row order. Empty exactly when that order is the
1289 /// consecutive identity `0..rows`.
1290 order_override: Vec<u32>,
1291 /// `(variable, end offset into the grouped order)`, in variable order.
1292 groups: Vec<(VariableId, usize)>,
1293}
1294
1295impl DepthPlan {
1296 /// Row visitation order denoted by this plan.
1297 fn selection(&self, rows: usize) -> RowSelection<'_> {
1298 match self {
1299 Self::Split(plan) if !plan.order_override.is_empty() => {
1300 debug_assert_eq!(plan.order_override.len(), rows);
1301 RowSelection::explicit(&plan.order_override)
1302 }
1303 Self::Unplanned | Self::Universal(_) | Self::Split(_) => RowSelection::identity(rows),
1304 }
1305 }
1306
1307 fn group_count(&self) -> usize {
1308 match self {
1309 Self::Unplanned => 0,
1310 Self::Universal(_) => 1,
1311 Self::Split(plan) => plan.groups.len(),
1312 }
1313 }
1314
1315 fn group(&self, group: usize, rows: usize) -> (VariableId, std::ops::Range<usize>) {
1316 match self {
1317 Self::Unplanned => panic!("an unplanned frontier has no variable groups"),
1318 Self::Universal(variable) => {
1319 assert_eq!(group, 0, "universal plan has exactly one group");
1320 (*variable, 0..rows)
1321 }
1322 Self::Split(plan) => {
1323 let start = if group == 0 {
1324 0
1325 } else {
1326 plan.groups[group - 1].1
1327 };
1328 let (variable, end) = plan.groups[group];
1329 (variable, start..end)
1330 }
1331 }
1332 }
1333}
1334
1335/// One frontier: the index matrix of the rows sitting at one point of the
1336/// search, their per-row estimates, and an exact generated-or-split plan.
1337#[derive(Clone, Debug, Default)]
1338struct Depth {
1339 /// Row-major index matrix, `rows * slots` entries.
1340 block: Arc<Vec<u32>>,
1341 rows: usize,
1342 /// Row-major estimate matrix, `rows * slots` entries.
1343 estimates: Arc<Vec<usize>>,
1344 plan: DepthPlan,
1345 /// Next group to expand.
1346 group: usize,
1347 /// Exclusive end of the group range this query owns.
1348 ///
1349 /// Normally this is `plan.group_count()`. A rayon sibling is fenced to one
1350 /// shared-plan group by setting `group_limit = group + 1`; the original
1351 /// advances `group` and retains everything after it. Keeping ownership as
1352 /// a cursor interval lets both halves share a fragmented [`SplitPlan`]
1353 /// without copying or truncating it.
1354 group_limit: usize,
1355 /// Emission cursor, used only when every variable is bound.
1356 emit: usize,
1357}
1358
1359impl Depth {
1360 fn selection(&self) -> RowSelection<'_> {
1361 self.plan.selection(self.rows)
1362 }
1363
1364 fn group(&self, group: usize) -> (VariableId, std::ops::Range<usize>) {
1365 self.plan.group(group, self.rows)
1366 }
1367}
1368
1369/// Observational counters for the batched search — how wide the frontier
1370/// actually got, and how often its rows disagreed about which variable to
1371/// bind next.
1372///
1373/// Fragmentation is the interesting number:
1374/// [`mean_variable_groups`](FrontierStats::mean_variable_groups) is 1.0 when
1375/// every row of every frontier agreed (the batch stayed whole) and rises as
1376/// bindings pull rows onto different adaptive choices. It is measured
1377/// rather than assumed because a row is never moved onto a variable it did
1378/// not choose — see [`Query::plan`].
1379///
1380/// Counters use relaxed atomics and are shared with every rayon clone of
1381/// the query, so a snapshot taken after the iterator is exhausted covers
1382/// the whole (possibly parallel) run.
1383#[derive(Debug, Default)]
1384pub struct FrontierStats {
1385 expansions: AtomicU64,
1386 rows: AtomicU64,
1387 variable_groups: AtomicU64,
1388 proposals: AtomicU64,
1389 peak_region: AtomicU64,
1390 widest: AtomicU64,
1391 inplace_descents: AtomicU64,
1392 copied_descents: AtomicU64,
1393}
1394
1395impl FrontierStats {
1396 /// Number of frontier expansions (one per batch of parent rows).
1397 pub fn expansions(&self) -> u64 {
1398 self.expansions.load(Ordering::Relaxed)
1399 }
1400
1401 /// Total parent rows expanded, summed over expansions.
1402 pub fn rows(&self) -> u64 {
1403 self.rows.load(Ordering::Relaxed)
1404 }
1405
1406 /// Total preferred-variable groups, summed over expansions. Equal to
1407 /// [`expansions`](Self::expansions) when no frontier ever fragmented.
1408 pub fn variable_groups(&self) -> u64 {
1409 self.variable_groups.load(Ordering::Relaxed)
1410 }
1411
1412 /// Total candidates proposed across all levels.
1413 pub fn proposals(&self) -> u64 {
1414 self.proposals.load(Ordering::Relaxed)
1415 }
1416
1417 /// Largest number of proposals materialised by one level at once.
1418 ///
1419 /// A refill proposes for every parent row in one preferred-variable
1420 /// group before the level is consumed in geometrically widening chunks.
1421 /// The resident proposal region can therefore be much larger than the
1422 /// frontier itself. Cumulative [`proposals`](Self::proposals) and row
1423 /// width [`widest`](Self::widest) measure different quantities; neither
1424 /// can be used to recover this proposal-memory high-water mark.
1425 pub fn peak_region(&self) -> u64 {
1426 self.peak_region.load(Ordering::Relaxed)
1427 }
1428
1429 /// Rows in the widest single expansion — the widest frontier the
1430 /// search actually reached.
1431 ///
1432 /// [`mean_width`](Self::mean_width) says what the typical expansion
1433 /// looked like; this says whether the ceiling was ever approached at
1434 /// all. The difference matters when reading a benchmark: a query whose
1435 /// widest frontier is far below `DEFAULT_FRONTIER_WIDTH` cannot
1436 /// demonstrate anything about batch-width thresholds, however the
1437 /// engine behaves — the data simply never filled a batch.
1438 pub fn widest(&self) -> u64 {
1439 self.widest.load(Ordering::Relaxed)
1440 }
1441
1442 /// Descents that reused the parent frontier's matrices in place — the
1443 /// 1:1 case, where the child block *is* the parent block with one more
1444 /// slot filled in and nothing is allocated or copied.
1445 pub fn inplace_descents(&self) -> u64 {
1446 self.inplace_descents.load(Ordering::Relaxed)
1447 }
1448
1449 /// Descents that had to build a fresh child frontier by copying each
1450 /// row of the parent.
1451 pub fn copied_descents(&self) -> u64 {
1452 self.copied_descents.load(Ordering::Relaxed)
1453 }
1454
1455 /// Mean rows per expansion — the width the search actually achieved.
1456 pub fn mean_width(&self) -> f64 {
1457 let expansions = self.expansions();
1458 if expansions == 0 {
1459 0.0
1460 } else {
1461 self.rows() as f64 / expansions as f64
1462 }
1463 }
1464
1465 /// Mean preferred-variable groups per expansion; 1.0 means no frontier
1466 /// ever fragmented.
1467 pub fn mean_variable_groups(&self) -> f64 {
1468 let expansions = self.expansions();
1469 if expansions == 0 {
1470 0.0
1471 } else {
1472 self.variable_groups() as f64 / expansions as f64
1473 }
1474 }
1475}
1476
1477// Manual `Clone` impl, because `#[derive(Clone)]` would require `R: Clone`
1478// which isn't actually needed — `R` only appears in `P`'s return type.
1479#[cfg(feature = "parallel")]
1480impl<C, P, R> Clone for Query<C, P, R>
1481where
1482 C: Clone,
1483 P: Fn(&Binding<'_>) -> Option<R> + Clone,
1484{
1485 fn clone(&self) -> Self {
1486 Self {
1487 constraint: self.constraint.clone(),
1488 postprocessing: self.postprocessing.clone(),
1489 mode: self.mode,
1490 // Consumption cursors are branch-local; immutable published
1491 // proposal buffers are shared by refcount.
1492 bindings: self.bindings.clone(),
1493 influences: self.influences,
1494 unbound: self.unbound,
1495 slots: self.slots,
1496 width: self.width,
1497 parallel: self.parallel,
1498 stack: self.stack.clone(),
1499 // Only the live prefix of the frontier stack matters; the
1500 // retired tail is scratch. Each `Depth`'s matrices sit behind
1501 // `Arc`, so transferring a planned group copies refcounts, not
1502 // megabytes — the copy-on-write lands on whichever owner
1503 // rewrites a frontier first.
1504 depths: self.depths[..=self.depth].to_vec(),
1505 depth: self.depth,
1506 choice: Vec::new(),
1507 drawn: Vec::new(),
1508 parents: Vec::new(),
1509 stats: Arc::clone(&self.stats),
1510 }
1511 }
1512}
1513
1514#[cfg(feature = "parallel")]
1515impl<C, P, R> Query<C, P, R>
1516where
1517 C: Clone,
1518 P: Fn(&Binding<'_>) -> Option<R> + Clone,
1519{
1520 /// Whether retiring the current frontier would expose work still owned
1521 /// by this query.
1522 ///
1523 /// A split is useful only when the left producer has a distinct future
1524 /// after transferring the right producer's frontier unit. That future
1525 /// can be a later group in an ancestor frontier, or candidates from the
1526 /// ancestor's already-proposed level that its geometric cursor has not
1527 /// consumed yet. `proposed > consumed` is intentionally conservative:
1528 /// the unseen suffix may contain only dead candidates, but discovering
1529 /// that is precisely the left continuation's work.
1530 fn has_ancestor_continuation(&self) -> bool {
1531 (0..self.depth).any(|depth| {
1532 let frontier = &self.depths[depth];
1533 frontier.group < frontier.group_limit
1534 || self
1535 .stack
1536 .get(depth)
1537 .is_some_and(|level| level.proposed > self.bindings.consumed(level.variable))
1538 })
1539 }
1540
1541 /// Transfers the next *whole planned group* to a fenced sibling query.
1542 ///
1543 /// A group is all rows of one frontier that chose the same next
1544 /// variable under the same ordered binding history. The sibling owns
1545 /// that group in its entirety; `self` advances past it and retains the
1546 /// continuation — later groups at this depth and, once they are spent,
1547 /// the ancestor source that can draw the next geometric page. No
1548 /// proposal buffer is bisected, so a source's batch remains intact.
1549 ///
1550 /// The sibling is re-rooted at the group's frontier and fenced there:
1551 /// ancestor `Level`s are discarded, while their `LevelValues` remain in
1552 /// the cloned [`BindingStore`] because the group's rows resolve bound
1553 /// indexes through them. Once the owned group is exhausted, the sibling
1554 /// ends instead of replaying an ancestor continuation still owned by
1555 /// `self`.
1556 ///
1557 /// A sole group at the root cannot be transferred: neither half would
1558 /// retain a distinct continuation, and repeated rayon splitting would
1559 /// merely hand the same indivisible work back and forth. Entering it
1560 /// serially may create child groups/pages that *do* have a continuation
1561 /// and can then be transferred safely.
1562 fn split_pending_group(&mut self) -> Option<Self> {
1563 debug_assert_eq!(self.mode, Search::NextGroup);
1564
1565 let depth = self.depth;
1566 let current = self.depths[depth].group;
1567 let group_limit = self.depths[depth].group_limit;
1568 let later_group = current + 1 < group_limit;
1569 if current >= group_limit || !(later_group || self.has_ancestor_continuation()) {
1570 return None;
1571 }
1572
1573 let mut right = self.clone();
1574 self.depths[depth].group += 1;
1575
1576 drop(right.stack.drain(..depth));
1577 drop(right.depths.drain(..depth));
1578 right.depth = 0;
1579
1580 let root = &mut right.depths[0];
1581 root.group_limit = root.group + 1;
1582 Some(right)
1583 }
1584
1585 /// Transfers the un-emitted remainder of a complete frontier page to a
1586 /// fenced sibling, leaving `self` at the parent source continuation.
1587 ///
1588 /// Terminal pages have no `NextGroup` state — planning moves them
1589 /// straight to [`Search::Emit`] — but they are still whole products of a
1590 /// planned group. Treating the page as one indivisible unit is what lets
1591 /// single-variable queries participate in rayon without slicing their
1592 /// proposal buffers or their result frontiers. The sibling preserves the
1593 /// current emission cursor, so the operation is correct even if a future
1594 /// caller exposes splitting after partial consumption.
1595 fn split_terminal_frontier(&mut self) -> Option<Self> {
1596 debug_assert_eq!(self.mode, Search::Emit);
1597 let depth = self.depth;
1598 if depth == 0
1599 || self.depths[depth].emit >= self.depths[depth].rows
1600 || !self.has_ancestor_continuation()
1601 {
1602 return None;
1603 }
1604
1605 let mut right = self.clone();
1606 self.depth -= 1;
1607 self.mode = Search::NextChunk;
1608
1609 drop(right.stack.drain(..depth));
1610 drop(right.depths.drain(..depth));
1611 right.depth = 0;
1612 Some(right)
1613 }
1614}
1615
1616impl<'a, C: Constraint<'a>, P: Fn(&Binding<'_>) -> Option<R>, R> Query<C, P, R> {
1617 /// Create a new query.
1618 /// The query takes a constraint and a post-processing function as input,
1619 /// and returns the results of the query as a stream of values.
1620 /// The post-processing function returns `Option<R>`: returning `None`
1621 /// skips the current binding and continues the search.
1622 ///
1623 /// This method is usually not called directly, but rather through the [find!] macro,
1624 pub fn new(constraint: C, postprocessing: P) -> Self {
1625 let variables = constraint.variables();
1626 let influences = std::array::from_fn(|v| {
1627 if variables.is_set(v) {
1628 constraint.influence(v)
1629 } else {
1630 VariableSet::new_empty()
1631 }
1632 });
1633 let bindings = BindingStore::new();
1634 let slots = variables.find_last_set().map(|v| v + 1).unwrap_or(0);
1635 let estimates: Vec<usize> = (0..slots)
1636 .map(|v| {
1637 if variables.is_set(v) {
1638 constraint
1639 .estimate(v, &bindings.view())
1640 .expect("unconstrained variable in query")
1641 } else {
1642 usize::MAX
1643 }
1644 })
1645 .collect();
1646
1647 // Constraints whose positions are all constant [`Term`]s (e.g. a
1648 // fully-constant `pattern!` used as an existence check) have an
1649 // empty variable set, so the propose/confirm search never consults
1650 // them. Their truth is binding-independent and `satisfied` is exact
1651 // for them from the start (the fully-bound exactness law: zero
1652 // unbound variables). One check up front settles every such
1653 // subtree; constraints with unbound variables answer an optimistic
1654 // `true` here and are validated by the search as usual.
1655 let mode = if constraint.satisfied(&bindings.view()) {
1656 Search::Plan
1657 } else {
1658 Search::Done
1659 };
1660
1661 let root = Depth {
1662 block: Arc::new(vec![0u32; slots]),
1663 rows: 1,
1664 estimates: Arc::new(estimates),
1665 plan: DepthPlan::Unplanned,
1666 group: 0,
1667 group_limit: 0,
1668 emit: 0,
1669 };
1670
1671 Query {
1672 constraint,
1673 postprocessing,
1674 mode,
1675 bindings,
1676 influences,
1677 unbound: variables,
1678 slots,
1679 width: DEFAULT_FRONTIER_WIDTH,
1680 parallel: false,
1681 stack: ArrayVec::new(),
1682 depths: vec![root],
1683 depth: 0,
1684 choice: Vec::new(),
1685 drawn: Vec::new(),
1686 parents: Vec::new(),
1687 stats: Arc::new(FrontierStats::default()),
1688 }
1689 }
1690
1691 /// Sets the *widest* frontier this query expands at once. See
1692 /// [`DEFAULT_FRONTIER_WIDTH`] for what the number buys and
1693 /// [`INITIAL_FRONTIER_WIDTH`] for the ramp that leads up to it.
1694 ///
1695 /// A width of 1 reduces the engine to expanding one binding at a time —
1696 /// the shape the protocol had before frontiers — which is what the
1697 /// equivalence tests pin. The ramp is a no-op at that ceiling, so the
1698 /// reduction is exact.
1699 ///
1700 /// # Panics
1701 ///
1702 /// Panics if `width` is zero.
1703 pub fn with_frontier_width(mut self, width: usize) -> Self {
1704 assert!(width > 0, "frontier width must be at least one binding");
1705 self.width = width;
1706 self
1707 }
1708
1709 /// Observational counters for this query's frontiers. The handle is
1710 /// shared with every rayon clone, so it stays meaningful under parallel
1711 /// execution.
1712 pub fn stats(&self) -> Arc<FrontierStats> {
1713 Arc::clone(&self.stats)
1714 }
1715
1716 /// The frontier currently at the top of the stack, unrestricted.
1717 fn frontier(&self) -> Frontier<'_> {
1718 let depth = &self.depths[self.depth];
1719 self.bindings
1720 .batch(&depth.block, depth.selection(), self.slots, self.parallel)
1721 }
1722
1723 /// Partitions the top frontier by each row's preferred variable.
1724 ///
1725 /// The preference is the same adaptive, magnitude-bucketed choice the
1726 /// engine has always made — smallest estimate magnitude, most
1727 /// influence — evaluated per row, against that row's own estimates.
1728 /// Rows are never moved onto a variable they did not choose:
1729 /// [`propose`](Constraint::propose) owns candidate support and
1730 /// first-seen order, and the protocol supplies no cross-variable
1731 /// support-equivalence law, so an estimate-compatible variable is not
1732 /// an interchangeable action. All the leeway lives in the magnitude
1733 /// bucketing, which is what makes agreement — and therefore a whole,
1734 /// unsplit batch — the common case.
1735 fn plan(&mut self) {
1736 if self.unbound.is_empty() {
1737 self.depths[self.depth].emit = 0;
1738 self.mode = Search::Emit;
1739 return;
1740 }
1741
1742 let slots = self.slots;
1743 let influences = &self.influences;
1744 let unbound = self.unbound;
1745 let depth = &mut self.depths[self.depth];
1746 let rows = depth.rows;
1747
1748 assert!(rows != 0, "the engine never plans an empty frontier");
1749 u32::try_from(rows - 1).expect("frontier row ordinal exceeds u32");
1750
1751 let preferred = |row: usize| {
1752 let row_estimates = &depth.estimates[row * slots..(row + 1) * slots];
1753 unbound
1754 .into_iter()
1755 .max_by_key(|&v| {
1756 (
1757 Reverse(
1758 row_estimates[v]
1759 .checked_ilog2()
1760 .map(|magnitude| magnitude + 1)
1761 .unwrap_or(0),
1762 ),
1763 influences[v].count(),
1764 )
1765 })
1766 .expect("non-empty unbound")
1767 };
1768
1769 self.choice.clear();
1770 let first = preferred(0);
1771 let disagreement = (1..rows).find_map(|row| {
1772 let variable = preferred(row);
1773 (variable != first).then_some((row, variable))
1774 });
1775
1776 if let Some((row, variable)) = disagreement {
1777 // Only an observed distinction earns storage. Materialise the
1778 // implied unanimous prefix at the first counterexample, then the
1779 // counterexample and remaining exact choices.
1780 self.choice.resize(
1781 row,
1782 u32::try_from(first).expect("query variables fit in u32"),
1783 );
1784 self.choice
1785 .push(u32::try_from(variable).expect("query variables fit in u32"));
1786 for row in row + 1..rows {
1787 self.choice
1788 .push(u32::try_from(preferred(row)).expect("query variables fit in u32"));
1789 }
1790
1791 let mut counts = [0usize; 128];
1792 for &variable in &self.choice {
1793 counts[variable as usize] += 1;
1794 }
1795 let mut starts = [0usize; 128];
1796 let mut split = SplitPlan::default();
1797 let mut offset = 0;
1798 for variable in 0..slots {
1799 starts[variable] = offset;
1800 if counts[variable] != 0 {
1801 offset += counts[variable];
1802 split.groups.push((variable, offset));
1803 }
1804 }
1805 debug_assert_eq!(offset, rows);
1806 debug_assert!(split.groups.len() > 1);
1807
1808 // Stable counting sort is the identity exactly when the input
1809 // choices are already in nondecreasing variable order.
1810 let identity = self.choice.windows(2).all(|pair| pair[0] <= pair[1]);
1811 if !identity {
1812 split.order_override.resize(rows, 0);
1813 for (row, &variable) in self.choice.iter().enumerate() {
1814 split.order_override[starts[variable as usize]] =
1815 u32::try_from(row).expect("frontier row ordinal exceeds u32");
1816 starts[variable as usize] += 1;
1817 }
1818 }
1819 depth.plan = DepthPlan::Split(Arc::new(split));
1820 } else {
1821 depth.plan = DepthPlan::Universal(first);
1822 }
1823 depth.group = 0;
1824 depth.group_limit = depth.plan.group_count();
1825
1826 self.stats.expansions.fetch_add(1, Ordering::Relaxed);
1827 self.stats.rows.fetch_add(rows as u64, Ordering::Relaxed);
1828 self.stats.widest.fetch_max(rows as u64, Ordering::Relaxed);
1829 self.stats.variable_groups.fetch_add(
1830 self.depths[self.depth].group_limit as u64,
1831 Ordering::Relaxed,
1832 );
1833 self.mode = Search::NextGroup;
1834 }
1835
1836 /// Expands the next group of the top frontier: pushes its variable and
1837 /// proposes candidates for every row of the group in one call. When the
1838 /// groups are exhausted the frontier itself is retired.
1839 fn next_group(&mut self) {
1840 let depth = &self.depths[self.depth];
1841 if depth.group >= depth.group_limit {
1842 if self.depth == 0 {
1843 self.mode = Search::Done;
1844 } else {
1845 self.depth -= 1;
1846 self.mode = Search::NextChunk;
1847 }
1848 return;
1849 }
1850 let group = depth.group;
1851 let (variable, range) = depth.group(group);
1852 self.depths[self.depth].group += 1;
1853
1854 self.unbound.unset(variable);
1855
1856 let constraint = &self.constraint;
1857 let depth = &self.depths[self.depth];
1858 let block = Arc::clone(&depth.block);
1859 let selection = depth.selection().slice(range);
1860 let proposed = self.bindings.refill(
1861 variable,
1862 &block,
1863 selection,
1864 self.slots,
1865 self.parallel,
1866 |frontier, proposals| constraint.propose(variable, frontier, proposals),
1867 );
1868 self.stack.push(Level {
1869 variable,
1870 width: INITIAL_FRONTIER_WIDTH.min(self.width),
1871 proposed,
1872 });
1873 self.stats
1874 .proposals
1875 .fetch_add(proposed as u64, Ordering::Relaxed);
1876 self.stats
1877 .peak_region
1878 .fetch_max(proposed as u64, Ordering::Relaxed);
1879 self.mode = Search::NextChunk;
1880 }
1881
1882 /// Consumes the next chunk of the top level's candidates into the child
1883 /// frontier. When the level is spent the group is retired and the next
1884 /// one gets its turn.
1885 ///
1886 /// # In-place 1:1 descent
1887 ///
1888 /// The child frontier is normally built by copying each drawn
1889 /// candidate's parent row and filling in the newly bound variable's
1890 /// slot. When the draw is **1:1** — one surviving child per parent row,
1891 /// in order, covering the whole parent frontier, with nothing left over
1892 /// — no row was gained, lost or reordered, so the child block *is* the
1893 /// parent block with one more slot written. The engine's two standing
1894 /// invariants are what make that safe: confirmers may only kill
1895 /// candidates and never revive them (so a surviving row keeps its
1896 /// identity), and buffers are write-once (so `variable`'s slot in every
1897 /// row was previously unwritten and filling it destroys nothing). The
1898 /// same argument covers the estimate matrix: the rows the child would
1899 /// have copied are bit-identical to the parent's, and the only slots it
1900 /// then overwrites are the ones the influence refresh recomputes
1901 /// anyway.
1902 ///
1903 /// A 1:1 draw that leaves the level spent is also the last thing the
1904 /// parent frontier is ever asked for, so the matrices are handed *down*
1905 /// (swapped with the child's retired allocations) rather than shared —
1906 /// which is what lets a whole chain of 1:1 descents run without a
1907 /// single matrix copy, instead of only the first one.
1908 ///
1909 /// Ownership is not tracked separately: the frontier matrices already
1910 /// sit behind `Arc` so a rayon split copies refcounts, and
1911 /// [`Arc::get_mut`] therefore succeeds exactly when no split or steal
1912 /// is holding the other half. Let the `Arc` be the guard; when it says
1913 /// no, take the copying path.
1914 ///
1915 /// **The fast path is gated so it costs nothing when it cannot fire.**
1916 /// Recognising a 1:1 draw means deferring the child rows until the
1917 /// draw's shape is known, and that deferral is a second pass — measured
1918 /// at +10% on F10 and +20% on F6 when charged to every descent. So the
1919 /// engine asks first, from what it already knows: a level holding
1920 /// `proposed` candidates for `rows` parents can only yield one child
1921 /// per parent if `proposed == rows`. Fan-out levels fail that `O(1)`
1922 /// test and run the fused [`take_chunk`](BindingStore::take_chunk)
1923 /// exactly as before.
1924 fn next_chunk(&mut self) {
1925 let level = *self.stack.last().expect("a level to chunk");
1926 let variable = level.variable;
1927 let parent = self.depth;
1928 let group = self.depths[parent].group - 1;
1929 let (_, range) = self.depths[parent].group(group);
1930 let slots = self.slots;
1931
1932 if self.depths.len() <= parent + 1 {
1933 self.depths.push(Depth::default());
1934 }
1935
1936 // Could this draw possibly be 1:1? One child per parent needs
1937 // exactly as many candidates as there are parent rows, and a chunk
1938 // wide enough to take them all. Both are known before drawing, and
1939 // both are `O(1)`.
1940 let source_rows = range.len();
1941 let speculate = level.proposed == source_rows
1942 && level.width >= source_rows
1943 && self.depths[parent].group >= self.depths[parent].group_limit;
1944
1945 let rows = if speculate {
1946 let selection = self.depths[parent].selection().slice(range.clone());
1947 self.bindings.draw(
1948 variable,
1949 level.width,
1950 selection,
1951 &mut self.drawn,
1952 &mut self.parents,
1953 );
1954 self.drawn.len()
1955 } else {
1956 let (head, tail) = self.depths.split_at_mut(parent + 1);
1957 let selection = head[parent].selection().slice(range);
1958 self.bindings.take_chunk(
1959 variable,
1960 level.width,
1961 &head[parent].block,
1962 selection,
1963 slots,
1964 Arc::make_mut(&mut tail[0].block),
1965 &mut self.parents,
1966 )
1967 };
1968 // Widen the next chunk this level hands down. The first chunk is one
1969 // binding, so a caller who stops after one row pays exactly what
1970 // depth-first paid; after that the width climbs by
1971 // `FRONTIER_RAMP_BASE` to the ceiling, which serves the caller who
1972 // wants a handful of rows without surrendering the peak (see
1973 // `FRONTIER_RAMP_BASE` for why the base is what decides that).
1974 let consumed = self.bindings.consumed(variable);
1975 if let Some(top) = self.stack.last_mut() {
1976 let next = top.width.saturating_mul(FRONTIER_RAMP_BASE).min(self.width);
1977 // Never leave a tail smaller than the chunk that would precede
1978 // it. `proposed - consumed` over-counts (it cannot see which
1979 // entries confirm already killed), so this merges conservatively
1980 // — it can decline a merge it should have made, never force one
1981 // it should not. Both branches are O(1): the level already knows
1982 // how many candidates it proposed and how far the cursor ran.
1983 let remaining = top.proposed.saturating_sub(consumed);
1984 top.width = if remaining < next.saturating_mul(2) {
1985 // `.min(self.width)` is load-bearing, not belt-and-braces:
1986 // `remaining` is bounded by the region, not by the ceiling,
1987 // so merging a tail without it hands down a chunk up to
1988 // twice the width the caller asked for. Measured before the
1989 // cap: 134 of 300 registry spans exceeded a 16384 ceiling,
1990 // worst at 1.93x — and `width` is a ceiling that the
1991 // `O(width · variables · depth)` frontier-memory bound is
1992 // stated against. At the ceiling the merge is a no-op, which
1993 // is right: a level already running flat has no ramp tail.
1994 remaining.max(next).min(self.width)
1995 } else {
1996 next
1997 };
1998 }
1999
2000 if rows == 0 {
2001 self.stack.pop();
2002 self.bindings.unset(variable);
2003 self.unbound.set(variable);
2004 self.mode = Search::NextGroup;
2005 return;
2006 }
2007
2008 // The speculation paid off when every parent really did produce
2009 // exactly one child, in order, and nothing is left pending.
2010 // `spent` is last because it scans the level's remaining liveness
2011 // words.
2012 let reusable = speculate
2013 && rows == source_rows
2014 && self
2015 .parents
2016 .iter()
2017 .enumerate()
2018 .all(|(row, &parent_row)| parent_row as usize == row)
2019 && self.bindings.spent(variable);
2020
2021 let (head, tail) = self.depths.split_at_mut(parent + 1);
2022 let source = &mut head[parent];
2023 let child = &mut tail[0];
2024
2025 let inplace = reusable
2026 && Arc::get_mut(&mut source.block).is_some()
2027 && Arc::get_mut(&mut source.estimates).is_some();
2028
2029 if inplace {
2030 // Hand the matrices down and take the child's retired
2031 // allocations in exchange, so the next 1:1 descent finds a
2032 // uniquely-owned block again and the chain stays copy-free.
2033 std::mem::swap(&mut source.block, &mut child.block);
2034 std::mem::swap(&mut source.estimates, &mut child.estimates);
2035 let block = Arc::get_mut(&mut child.block).expect("unique across the swap");
2036 for (row, &entry) in self.drawn.iter().enumerate() {
2037 block[row * slots + variable] = entry;
2038 }
2039 self.stats.inplace_descents.fetch_add(1, Ordering::Relaxed);
2040 } else {
2041 if speculate {
2042 // Speculated and lost: the rows still have to be written,
2043 // just from the deferred draw rather than fused with it.
2044 let block = reset_shared(&mut child.block, rows * slots);
2045 for (&entry, &parent_row) in self.drawn.iter().zip(self.parents.iter()) {
2046 let parent_row = parent_row as usize;
2047 block.extend_from_slice(
2048 &source.block[parent_row * slots..(parent_row + 1) * slots],
2049 );
2050 let base = block.len() - slots;
2051 block[base + variable] = entry;
2052 }
2053 }
2054 // Inherit each child row's estimates from its parent; the
2055 // refresh below then updates exactly the ones binding
2056 // `variable` can have changed — the same influence-driven
2057 // refresh the single-binding engine did, now once per row of
2058 // the batch. Nothing else can have gone stale: a row's
2059 // estimates were computed against its own binding, so
2060 // backtracking never invalidates them.
2061 let estimates = reset_shared(&mut child.estimates, rows * slots);
2062 for &parent_row in self.parents.iter() {
2063 let parent_row = parent_row as usize;
2064 estimates.extend_from_slice(
2065 &source.estimates[parent_row * slots..(parent_row + 1) * slots],
2066 );
2067 }
2068 self.stats.copied_descents.fetch_add(1, Ordering::Relaxed);
2069 }
2070 child.rows = rows;
2071 child.plan = DepthPlan::Unplanned;
2072 child.group = 0;
2073 child.group_limit = 0;
2074 child.emit = 0;
2075
2076 let stale = self.influences[variable].intersect(self.unbound);
2077 if !stale.is_empty() {
2078 let child = &mut self.depths[parent + 1];
2079 let block = Arc::clone(&child.block);
2080 let estimates = Arc::make_mut(&mut child.estimates);
2081 let batch =
2082 self.bindings
2083 .batch(&block, RowSelection::identity(rows), slots, self.parallel);
2084 for row in 0..rows {
2085 let binding = batch.row(row);
2086 for v in stale {
2087 estimates[row * slots + v] = self
2088 .constraint
2089 .estimate(v, &binding)
2090 .expect("unconstrained variable in query");
2091 }
2092 }
2093 }
2094
2095 self.depth = parent + 1;
2096 self.mode = Search::Plan;
2097 }
2098}
2099
2100/// The search mode of the query engine.
2101///
2102/// The engine is still a depth-first search; what moved is its unit of
2103/// work. A step no longer binds one variable to one value — it expands a
2104/// whole *frontier* of parent bindings at once, so every level's proposal
2105/// region is as wide as the batch rather than as wide as one parent's
2106/// candidate list.
2107///
2108/// - `Plan` — partition the top frontier by each row's preferred variable.
2109/// - `NextGroup` — expand the next group: push its variable and propose for
2110/// all its rows in one call.
2111/// - `NextChunk` — turn the next `width` surviving candidates into the
2112/// child frontier, or retire the group when they run out.
2113/// - `Emit` — every variable is bound; stream the frontier's rows out.
2114/// - `Done` — the search is finished.
2115#[derive(Copy, Clone, Debug, Eq, PartialEq)]
2116enum Search {
2117 Plan,
2118 NextGroup,
2119 NextChunk,
2120 Emit,
2121 Done,
2122}
2123
2124impl<'a, C: Constraint<'a>, P: Fn(&Binding<'_>) -> Option<R>, R> Iterator for Query<C, P, R> {
2125 type Item = R;
2126
2127 fn next(&mut self) -> Option<Self::Item> {
2128 loop {
2129 match self.mode {
2130 Search::Plan => self.plan(),
2131 Search::NextGroup => self.next_group(),
2132 Search::NextChunk => self.next_chunk(),
2133 Search::Emit => {
2134 // One row per complete binding, in frontier order, no
2135 // dedup — bag semantics are a property of the rows, not
2136 // of how many at a time the engine happened to build.
2137 let frontier = self.frontier();
2138 let rows = frontier.len();
2139 let mut emit = self.depths[self.depth].emit;
2140 let mut result = None;
2141 while emit < rows {
2142 let binding = frontier.row(emit);
2143 emit += 1;
2144 if let Some(row) = (self.postprocessing)(&binding) {
2145 result = Some(row);
2146 break;
2147 }
2148 }
2149 self.depths[self.depth].emit = emit;
2150 if result.is_some() {
2151 return result;
2152 }
2153 // The batch is spent; hand control back to the level
2154 // that produced it for its next chunk.
2155 if self.depth == 0 {
2156 self.mode = Search::Done;
2157 } else {
2158 self.depth -= 1;
2159 self.mode = Search::NextChunk;
2160 }
2161 }
2162 Search::Done => return None,
2163 }
2164 }
2165 }
2166}
2167
2168impl<'a, C: Constraint<'a>, P: Fn(&Binding<'_>) -> Option<R>, R> fmt::Debug for Query<C, P, R> {
2169 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2170 f.debug_struct("Query")
2171 .field("constraint", &std::any::type_name::<C>())
2172 .field("mode", &self.mode)
2173 .field("width", &self.width)
2174 .field("frontier", &self.frontier())
2175 .field("stack", &self.stack)
2176 .field("unbound", &self.unbound)
2177 .finish()
2178 }
2179}
2180
2181// ---------------------------------------------------------------------------
2182// Parallel execution via rayon.
2183//
2184// `Query` implements `IntoParallelIterator` with `Iter = QueryParIter`.
2185// `QueryParIter` is a separate wrapper type implementing `ParallelIterator`
2186// + `UnindexedProducer`, distinct from `Query` itself to avoid method-name
2187// ambiguity between `Iterator` and `ParallelIterator` — methods like
2188// `.count()`, `.collect()`, `.map()` exist on both.
2189//
2190// Usage: `find!(...).into_par_iter().map(...).collect::<Vec<_>>()`.
2191//
2192// The producer's `split` transfers one *whole planned frontier group* to a
2193// sibling. A group is the rows sharing both an ordered variable-choice prefix
2194// and their preferred next variable. The original producer skips that group
2195// and retains the source continuation, so it can draw the next geometric page
2196// while the sibling evaluates the current one. The sibling is re-rooted and
2197// fenced at the group: it cannot replay later groups or ancestor work.
2198//
2199// No proposal buffer is bisected. This is the load-bearing difference from
2200// candidate sharding: every owned group reaches `propose`/`confirm` at its
2201// natural width, preserving geometric widening and accelerator-sized batches.
2202// A heavy sole root group is honestly serial until executing it produces a
2203// child group with a source continuation; the parallel path does not invent a
2204// destructive intra-group split merely to keep rayon busy.
2205//
2206// A built-in CPU constraint may nevertheless expose *work inside one logical
2207// confirm call* to the same pool. `IntoParallelIterator` marks the query's
2208// frontier views with crate-private execution intent; ordinary `Iterator`
2209// queries do not acquire that intent merely because they happen to run on a
2210// Rayon worker. TribleSet confirmation uses it to divide packed liveness at
2211// word boundaries, where each worker owns disjoint killable words. That leaves
2212// the frontier, proposal buffer, and any accelerator routing whole: it is data
2213// parallelism inside a leaf, not another search-state split.
2214// Every successful split advances the original owner's group/page cursor and
2215// fences that unit out of the sibling's future, so ownership is a strict
2216// progress measure. Rayon's pressure splitter needs no engine-specific split
2217// budget.
2218//
2219// `fold_with` is the terminal leaf: it just drives the existing sequential
2220// `Iterator::next()` and feeds results into the folder. No duplicated
2221// execution logic.
2222// ---------------------------------------------------------------------------
2223
2224#[cfg(feature = "parallel")]
2225pub use parallel::QueryParIter;
2226
2227#[cfg(feature = "parallel")]
2228mod parallel {
2229 use super::*;
2230 use rayon::iter::plumbing::{bridge_unindexed, Folder, UnindexedConsumer, UnindexedProducer};
2231 use rayon::iter::{IntoParallelIterator, ParallelIterator};
2232
2233 /// Parallel iterator over the results of a [`Query`]. Obtained via
2234 /// [`IntoParallelIterator::into_par_iter`] on a `Query`.
2235 ///
2236 /// Drives rayon's work-stealing scheduler through an `UnindexedProducer`
2237 /// impl on the underlying query state. The sequential `Iterator::next`
2238 /// on `Query` is reused as the fold leaf — parallel execution is purely
2239 /// additional, no duplicated engine logic.
2240 ///
2241 /// The inner query is stored in a [`Box`] so rayon's work-stealing
2242 /// `split` (which clones the producer) doesn't memcpy the query's large
2243 /// fixed state on every fork — just a Box pointer copy, with the heap
2244 /// allocation paid only by the child.
2245 pub struct QueryParIter<C, P: Fn(&Binding<'_>) -> Option<R>, R> {
2246 inner: Box<Query<C, P, R>>,
2247 }
2248
2249 impl<'a, C, P, R> IntoParallelIterator for Query<C, P, R>
2250 where
2251 C: Constraint<'a> + Clone + Send + 'a,
2252 P: Fn(&Binding<'_>) -> Option<R> + Clone + Send,
2253 R: Send,
2254 {
2255 type Item = R;
2256 type Iter = QueryParIter<C, P, R>;
2257
2258 fn into_par_iter(mut self) -> Self::Iter {
2259 self.parallel = true;
2260 QueryParIter {
2261 inner: Box::new(self),
2262 }
2263 }
2264 }
2265
2266 impl<'a, C, P, R> UnindexedProducer for QueryParIter<C, P, R>
2267 where
2268 C: Constraint<'a> + Clone + Send + 'a,
2269 P: Fn(&Binding<'_>) -> Option<R> + Clone + Send,
2270 R: Send,
2271 {
2272 type Item = R;
2273
2274 /// Advance the Query's state machine until either a whole planned
2275 /// group can be transferred to a sibling or the sub-query is
2276 /// exhausted / has reached a batch of complete rows (return `None`,
2277 /// leaving `self` as a leaf that `fold_with` will fold sequentially).
2278 fn split(mut self) -> (Self, Option<Self>) {
2279 let q = &mut *self.inner;
2280 loop {
2281 match q.mode {
2282 Search::Plan => q.plan(),
2283 Search::NextGroup => {
2284 if let Some(right) = q.split_pending_group() {
2285 return (
2286 self,
2287 Some(QueryParIter {
2288 inner: Box::new(right),
2289 }),
2290 );
2291 }
2292 q.next_group();
2293 }
2294 Search::Emit => {
2295 if let Some(right) = q.split_terminal_frontier() {
2296 return (
2297 self,
2298 Some(QueryParIter {
2299 inner: Box::new(right),
2300 }),
2301 );
2302 }
2303 return (self, None);
2304 }
2305 Search::Done => return (self, None),
2306 Search::NextChunk => {
2307 // Draw the next geometric page intact. Planning its
2308 // child frontier exposes one or more whole groups on
2309 // the next loop iteration; those are the stealable
2310 // units, not slices of this proposal buffer.
2311 q.next_chunk();
2312 }
2313 }
2314 }
2315 }
2316
2317 fn fold_with<F: Folder<R>>(self, mut folder: F) -> F {
2318 let QueryParIter { inner: mut q } = self;
2319 while !folder.full() {
2320 match q.next() {
2321 Some(item) => folder = folder.consume(item),
2322 None => break,
2323 }
2324 }
2325 folder
2326 }
2327 }
2328
2329 impl<'a, C, P, R> ParallelIterator for QueryParIter<C, P, R>
2330 where
2331 C: Constraint<'a> + Clone + Send + 'a,
2332 P: Fn(&Binding<'_>) -> Option<R> + Clone + Send,
2333 R: Send,
2334 {
2335 type Item = R;
2336
2337 fn drive_unindexed<Con>(self, consumer: Con) -> Con::Result
2338 where
2339 Con: UnindexedConsumer<Self::Item>,
2340 {
2341 bridge_unindexed(self, consumer)
2342 }
2343 }
2344}
2345
2346/// Iterate over query results, converting each variable via
2347/// [`TryFromInline`](crate::inline::TryFromInline).
2348///
2349/// The macro takes two arguments: a tuple of variables with optional type
2350/// annotations, and a constraint expression. It injects a `__local_find_context!`
2351/// macro that provides the variable context to nested query macros like
2352/// [`pattern!`](crate::macros::pattern) and [`temp!`](crate::temp).
2353///
2354/// # Variable syntax
2355///
2356/// | Syntax | Meaning |
2357/// |--------|---------|
2358/// | `name` | inferred type, filter on conversion failure |
2359/// | `name: Type` | explicit type, filter on conversion failure |
2360/// | `name?` | inferred type, yield `Result<T, E>` (no filter) |
2361/// | `name: Type?` | explicit type, yield `Result<T, E>` (no filter) |
2362///
2363/// Query heads have BAG semantics: one row per complete satisfying assignment.
2364/// Two assignments that agree on every declared head variable but differ in
2365/// hidden variables therefore produce two rows — hidden-variable multiplicity
2366/// is visible in the output, and the engine performs no deduplication. That is
2367/// deliberate: deduplicating would mean carrying a claim table proportional to
2368/// the distinct result set, sharing it across rayon shards, and deciding what a
2369/// panicking or filtered row does to a claimed key. Dedup is the consumer's
2370/// choice instead: collect into a set, or use two queries (an outer enumeration
2371/// over the values you want distinct, with an inner [`exists!`](crate::exists)
2372/// for the witness). Every projected variable must be unique; repeating a
2373/// variable in the head is a compile error because it would not add a column.
2374///
2375/// The unit form `find!((), constraint)` projects no variables and yields one
2376/// `()` per satisfying assignment — it counts witnesses rather than answering a
2377/// yes/no question. Use [`exists!`](crate::exists) for existence, which stops
2378/// at the first witness.
2379///
2380/// **Filter semantics (default):** when a variable's conversion fails the
2381/// entire row is silently skipped — like a constraint that doesn't match.
2382/// For types whose `TryFromInline::Error = Infallible` the error branch is
2383/// dead code, so no rows can ever be accidentally filtered.
2384///
2385/// **`?` pass-through:** appending `?` to a variable makes it yield
2386/// `Result<T, E>` directly. Both `Ok` and `Err` values pass through with
2387/// no filtering, matching Rust's `?` semantics of "bubble the error to the
2388/// caller."
2389///
2390/// # Examples
2391///
2392/// ```
2393/// # use triblespace_core::prelude::*;
2394/// # use triblespace_core::prelude::inlineencodings::ShortString;
2395/// // Filter semantics — rows where conversion fails are skipped:
2396/// let results = find!((x: Inline<ShortString>), x.is("foo".to_inline())).collect::<Vec<_>>();
2397/// ```
2398#[macro_export]
2399macro_rules! find {
2400 ($($tokens:tt)*) => {
2401 {
2402 #[allow(unused_mut, unused_variables)]
2403 let mut ctx = $crate::query::VariableContext::new();
2404
2405 macro_rules! __local_find_context {
2406 () => { &mut ctx }
2407 }
2408
2409 $crate::macros::__find_impl!($crate, ctx, $($tokens)*)
2410 }
2411 };
2412}
2413/// Re-export of the [`find!`] macro.
2414pub use find;
2415
2416/// Returns `true` when a query produces at least one row.
2417///
2418/// This is equivalent to calling `find!(...).next().is_some()`, but reads more
2419/// directly for existence checks.
2420///
2421/// # Forms
2422///
2423/// - `exists!(constraint)` checks a pure constraint with no projected
2424/// variables.
2425/// - `exists!((vars...), constraint)` uses the same variable/conversion syntax
2426/// as [`find!`] before checking whether any row survives projection.
2427///
2428/// ```rust,ignore
2429/// exists!(pattern!(&kb, [{ ?person @ social::name: "Alice" }]))
2430/// ```
2431///
2432/// ```rust,ignore
2433/// exists!(
2434/// (name: Inline<_>),
2435/// pattern!(&kb, [{ ?person @ social::name: ?name }])
2436/// )
2437/// ```
2438#[macro_export]
2439macro_rules! exists {
2440 (($($vars:tt)*), $Constraint:expr) => {
2441 $crate::query::find!(($($vars)*), $Constraint).next().is_some()
2442 };
2443 ($Constraint:expr) => {
2444 $crate::query::find!((), $Constraint).next().is_some()
2445 };
2446}
2447/// Re-export of the [`exists!`] macro.
2448pub use exists;
2449
2450/// Introduces one or more temporary query variables for a nested constraint.
2451///
2452/// `temp!` is only meaningful inside macros that provide a local query context,
2453/// such as [`find!`], [`exists!`], or macros expanded from them like
2454/// [`pattern!`](crate::macros::pattern). Each identifier becomes a fresh query
2455/// variable that is scoped to the wrapped body.
2456///
2457/// ```rust,ignore
2458/// find!(
2459/// (person: Inline<_>),
2460/// temp!((friend), and!(
2461/// pattern!(&kb, [{ ?person @ social::friend: ?friend }]),
2462/// pattern!(&kb, [{ ?friend @ social::name: "Bob" }])
2463/// ))
2464/// )
2465/// ```
2466#[macro_export]
2467macro_rules! temp {
2468 (($Var:ident), $body:expr) => {{
2469 let $Var = __local_find_context!().next_variable();
2470 $body
2471 }};
2472 (($Var:ident,), $body:expr) => {
2473 $crate::temp!(($Var), $body)
2474 };
2475 (($Var:ident, $($rest:ident),+ $(,)?), $body:expr) => {{
2476 $crate::temp!(
2477 ($Var),
2478 $crate::temp!(($($rest),+), $body)
2479 )
2480 }};
2481}
2482/// Re-export of the [`temp!`] macro.
2483pub use temp;
2484
2485#[cfg(test)]
2486mod tests {
2487 use inlineencodings::ShortString;
2488
2489 use crate::prelude::inlineencodings::*;
2490 use crate::prelude::*;
2491
2492 use crate::examples::literature;
2493
2494 use fake::faker::lorem::en::Sentence;
2495 use fake::faker::lorem::en::Words;
2496 use fake::faker::name::raw::*;
2497 use fake::locales::*;
2498 use fake::Fake;
2499
2500 use std::collections::HashSet;
2501
2502 use super::*;
2503
2504 fn variable_set(indices: impl IntoIterator<Item = VariableId>) -> VariableSet {
2505 let mut variables = VariableSet::new_empty();
2506 for variable in indices {
2507 variables.set(variable);
2508 }
2509 variables
2510 }
2511
2512 pub mod knights {
2513 use crate::prelude::*;
2514
2515 attributes! {
2516 "8143F46E812E88C4544E7094080EC523" unsafe as loves: inlineencodings::GenId;
2517 "D6E0F2A6E5214E1330565B4D4138E55C" unsafe as name: inlineencodings::ShortString;
2518 }
2519 }
2520
2521 mod shared_level_buffers {
2522 use super::*;
2523
2524 fn value(byte: u8) -> RawInline {
2525 let mut value = [0; 32];
2526 value[31] = byte;
2527 value
2528 }
2529
2530 fn refill(store: &mut BindingStore, values: &[RawInline]) {
2531 store.refill(
2532 0,
2533 &[0],
2534 RowSelection::identity(1),
2535 1,
2536 false,
2537 |frontier, proposals| {
2538 assert_eq!(frontier.len(), 1);
2539 proposals.open(0);
2540 proposals.extend(values.iter().copied());
2541 },
2542 );
2543 }
2544
2545 #[test]
2546 fn clone_shares_published_buffer_but_not_consumption_cursor() {
2547 let mut left = BindingStore::new();
2548 refill(&mut left, &[value(1), value(2), value(3)]);
2549 let right = left.clone();
2550
2551 assert!(Arc::ptr_eq(
2552 left.levels[0].buffer.as_ref().unwrap(),
2553 right.levels[0].buffer.as_ref().unwrap(),
2554 ));
2555
2556 let mut drawn = Vec::new();
2557 let mut parents = Vec::new();
2558 left.draw(0, 1, RowSelection::identity(1), &mut drawn, &mut parents);
2559 assert_eq!(left.levels[0].pos, 1);
2560 assert_eq!(right.levels[0].pos, 0);
2561 assert_eq!(right.levels[0].buffer()[0], value(1));
2562 }
2563
2564 #[test]
2565 fn refill_replaces_a_shared_buffer_without_touching_its_sibling() {
2566 let mut left = BindingStore::new();
2567 refill(&mut left, &[value(1), value(2)]);
2568 let right = left.clone();
2569
2570 refill(&mut left, &[value(9)]);
2571
2572 assert!(!Arc::ptr_eq(
2573 left.levels[0].buffer.as_ref().unwrap(),
2574 right.levels[0].buffer.as_ref().unwrap(),
2575 ));
2576 assert_eq!(&right.levels[0].buffer()[..], &[value(1), value(2)]);
2577 assert_eq!(&left.levels[0].buffer()[..], &[value(9)]);
2578 }
2579
2580 #[test]
2581 fn refill_reuses_a_uniquely_owned_buffer() {
2582 let mut store = BindingStore::new();
2583 refill(&mut store, &[value(1), value(2), value(3)]);
2584 let allocation = Arc::as_ptr(store.levels[0].buffer.as_ref().unwrap());
2585
2586 refill(&mut store, &[value(4)]);
2587
2588 assert_eq!(
2589 Arc::as_ptr(store.levels[0].buffer.as_ref().unwrap()),
2590 allocation,
2591 "unique refill should retain the ProposalBuffer allocation",
2592 );
2593 assert_eq!(&store.levels[0].buffer()[..], &[value(4)]);
2594 }
2595 }
2596
2597 mod social {
2598 use crate::prelude::*;
2599
2600 attributes! {
2601 "A19EC1D9DD534BA9896223A457A6B9C9" unsafe as name: inlineencodings::ShortString;
2602 "C21DE0AA5BA3446AB886C9640BA60244" unsafe as friend: inlineencodings::GenId;
2603 }
2604 }
2605
2606 #[test]
2607 fn and_set() {
2608 let mut books = HashSet::<String>::new();
2609 let mut movies = HashSet::<Inline<ShortString>>::new();
2610
2611 books.insert("LOTR".to_string());
2612 books.insert("Dragonrider".to_string());
2613 books.insert("Highlander".to_string());
2614
2615 movies.insert("LOTR".to_inline());
2616 movies.insert("Highlander".to_inline());
2617
2618 let inter: Vec<_> =
2619 find!((a: Inline<ShortString>), and!(books.has(a), movies.has(a))).collect();
2620
2621 assert_eq!(inter.len(), 2);
2622
2623 let cross: Vec<_> =
2624 find!((a: Inline<ShortString>, b: Inline<ShortString>), and!(books.has(a), movies.has(b))).collect();
2625
2626 assert_eq!(cross.len(), 6);
2627
2628 let one: Vec<_> = find!((a: Inline<ShortString>),
2629 and!(books.has(a), a.is(ShortString::inline_from("LOTR")))
2630 )
2631 .collect();
2632
2633 assert_eq!(one.len(), 1);
2634 }
2635
2636 #[test]
2637 fn pattern() {
2638 let mut kb = TribleSet::new();
2639 (0..1000).for_each(|_| {
2640 let author = fucid();
2641 let book = fucid();
2642 kb += entity! { &author @
2643 literature::firstname: FirstName(EN).fake::<String>(),
2644 literature::lastname: LastName(EN).fake::<String>(),
2645 };
2646 kb += entity! { &book @
2647 literature::author: &author,
2648 literature::title: Words(1..3).fake::<Vec<String>>().join(" "),
2649 literature::quote: Sentence(5..25).fake::<String>().to_blob().get_handle()
2650 };
2651 });
2652
2653 let author = fucid();
2654 let book = fucid();
2655 kb += entity! { &author @
2656 literature::firstname: "Frank",
2657 literature::lastname: "Herbert",
2658 };
2659 kb += entity! { &book @
2660 literature::author: &author,
2661 literature::title: "Dune",
2662 literature::quote: "I must not fear. Fear is the \
2663 mind-killer. Fear is the little-death that brings total \
2664 obliteration. I will face my fear. I will permit it to \
2665 pass over me and through me. And when it has gone past I \
2666 will turn the inner eye to see its path. Where the fear \
2667 has gone there will be nothing. Only I will remain.".to_blob().get_handle()
2668 };
2669
2670 (0..100).for_each(|_| {
2671 let author = fucid();
2672 let book = fucid();
2673 kb += entity! { &author @
2674 literature::firstname: "Fake",
2675 literature::lastname: "Herbert",
2676 };
2677 kb += entity! { &book @
2678 literature::author: &author,
2679 literature::title: Words(1..3).fake::<Vec<String>>().join(" "),
2680 literature::quote: Sentence(5..25).fake::<String>().to_blob().get_handle()
2681 };
2682 });
2683
2684 let r: Vec<_> = find!(
2685 (author: Inline<_>, book: Inline<_>, title: Inline<_>, quote: Inline<_>),
2686 pattern!(&kb, [
2687 {?author @
2688 literature::firstname: "Frank",
2689 literature::lastname: "Herbert"},
2690 {?book @
2691 literature::author: ?author,
2692 literature::title: ?title,
2693 literature::quote: ?quote
2694 }]))
2695 .collect();
2696
2697 assert_eq!(1, r.len())
2698 }
2699
2700 #[test]
2701 fn constant() {
2702 let r: Vec<_> = find! {
2703 (string: Inline<_>, number: Inline<_>),
2704 and!(
2705 string.is(ShortString::inline_from("Hello World!")),
2706 number.is(I256BE::inline_from(42))
2707 )
2708 }
2709 .collect();
2710
2711 assert_eq!(1, r.len())
2712 }
2713
2714 #[test]
2715 fn exists_true() {
2716 assert!(exists!((a: Inline<_>), a.is(I256BE::inline_from(42))));
2717 }
2718
2719 #[test]
2720 fn exists_false() {
2721 assert!(!exists!(
2722 (a: Inline<_>),
2723 and!(a.is(I256BE::inline_from(1)), a.is(I256BE::inline_from(2)))
2724 ));
2725 }
2726
2727 #[test]
2728 fn exists_no_variables_true() {
2729 let mut ctx = VariableContext::new();
2730 let a = ctx.next_variable::<I256BE>();
2731 assert!(exists!(a.is(I256BE::inline_from(42))));
2732 }
2733
2734 #[test]
2735 fn find_no_variables_yields_unit() {
2736 let mut ctx = VariableContext::new();
2737 let a = ctx.next_variable::<I256BE>();
2738 let rows: Vec<()> = find!((), a.is(I256BE::inline_from(42))).collect();
2739 assert_eq!(rows, vec![()]);
2740 }
2741
2742 #[test]
2743 fn temp_variables_span_patterns() {
2744 use social::*;
2745
2746 let mut kb = TribleSet::new();
2747 let alice = fucid();
2748 let bob = fucid();
2749
2750 kb += entity! { &alice @ name: "Alice", friend: &bob };
2751 kb += entity! { &bob @ name: "Bob" };
2752
2753 let matches: Vec<_> = find!(
2754 (person_name: Inline<_>),
2755 temp!((mutual_friend),
2756 and!(
2757 pattern!(&kb, [{ _?person @ name: ?person_name, friend: ?mutual_friend }]),
2758 pattern!(&kb, [{ ?mutual_friend @ name: "Bob" }])
2759 )
2760 )
2761 )
2762 .collect();
2763
2764 assert_eq!(matches.len(), 1);
2765 assert_eq!(matches[0].0.try_from_inline::<&str>().unwrap(), "Alice");
2766 }
2767
2768 #[test]
2769 fn estimate_override_debug_order() {
2770 use std::cell::RefCell;
2771 use std::rc::Rc;
2772
2773 let mut ctx = VariableContext::new();
2774 let a = ctx.next_variable::<ShortString>();
2775 let b = ctx.next_variable::<ShortString>();
2776
2777 let base = and!(
2778 a.is(ShortString::inline_from("A")),
2779 b.is(ShortString::inline_from("B"))
2780 );
2781
2782 let mut wrapper = crate::debug::query::EstimateOverrideConstraint::new(base);
2783 wrapper.set_estimate(a.index, 10);
2784 wrapper.set_estimate(b.index, 1);
2785
2786 let record = Rc::new(RefCell::new(Vec::new()));
2787 let debug = crate::debug::query::DebugConstraint::new(wrapper, Rc::clone(&record));
2788
2789 let q: Query<_, _, _> = Query::new(debug, |_| Some(()));
2790 let r: Vec<_> = q.collect();
2791 assert_eq!(1, r.len());
2792 assert_eq!(&*record.borrow(), &[b.index, a.index]);
2793 }
2794
2795 #[cfg(feature = "parallel")]
2796 mod split_fence {
2797 use super::*;
2798 use rayon::iter::plumbing::UnindexedProducer;
2799 use rayon::iter::{IntoParallelIterator, ParallelIterator};
2800 use std::sync::atomic::{AtomicU8, Ordering};
2801
2802 #[derive(Clone)]
2803 struct Fixture {
2804 fanout: u32,
2805 anchors: [u8; 4],
2806 }
2807
2808 /// One proposal per level and no alternate continuation anywhere.
2809 /// This is the shape on which speculative splitting is most harmful:
2810 /// cloning a query at every depth prevents the frontier matrices from
2811 /// descending in place even though no parallel work exists.
2812 #[derive(Clone)]
2813 struct OneToOne(usize);
2814
2815 /// Records whether the frontier handed to a constraint carried the
2816 /// explicit parallel-query intent. Bit 0 is sequential, bit 1 is
2817 /// parallel.
2818 #[derive(Clone)]
2819 struct IntentRecorder(Arc<AtomicU8>);
2820
2821 impl<'a> Constraint<'a> for IntentRecorder {
2822 fn variables(&self) -> VariableSet {
2823 variable_set([0])
2824 }
2825
2826 fn estimate(&self, variable: VariableId, _binding: &Binding) -> Option<usize> {
2827 (variable == 0).then_some(1)
2828 }
2829
2830 fn propose(
2831 &self,
2832 _variable: VariableId,
2833 frontier: &Frontier<'_>,
2834 proposals: &mut ProposalBuffer,
2835 ) {
2836 self.0.fetch_or(
2837 if frontier.parallel() { 0b10 } else { 0b01 },
2838 Ordering::Relaxed,
2839 );
2840 for row in 0..frontier.len() {
2841 proposals.open(row as u32);
2842 proposals.push(Fixture::value(0x55, row as u32));
2843 }
2844 }
2845
2846 fn confirm(
2847 &self,
2848 _variable: VariableId,
2849 _frontier: &Frontier<'_>,
2850 _candidates: &mut Candidates<'_>,
2851 ) {
2852 }
2853 }
2854
2855 impl<'a> Constraint<'a> for OneToOne {
2856 fn variables(&self) -> VariableSet {
2857 variable_set(0..self.0)
2858 }
2859
2860 fn estimate(&self, variable: VariableId, binding: &Binding) -> Option<usize> {
2861 (variable < self.0).then_some(if variable == binding.bound.count() {
2862 1
2863 } else {
2864 1 << 20
2865 })
2866 }
2867
2868 fn propose(
2869 &self,
2870 variable: VariableId,
2871 frontier: &Frontier<'_>,
2872 proposals: &mut ProposalBuffer,
2873 ) {
2874 for row in 0..frontier.len() {
2875 proposals.open(row as u32);
2876 proposals.push(Fixture::value(0x7f, variable as u32));
2877 }
2878 }
2879
2880 fn confirm(
2881 &self,
2882 _variable: VariableId,
2883 _frontier: &Frontier<'_>,
2884 _candidates: &mut Candidates<'_>,
2885 ) {
2886 }
2887 }
2888
2889 impl Fixture {
2890 fn identity_split(fanout: u32) -> Self {
2891 Self {
2892 fanout,
2893 // Anchor 3 is consumed by the scalar latency page. The
2894 // remaining choices are 1, 2, 3, so stable grouping is
2895 // already consecutive identity.
2896 anchors: [3, 0, 1, 2],
2897 }
2898 }
2899
2900 fn nonidentity_split(fanout: u32) -> Self {
2901 Self {
2902 fanout,
2903 // After anchor 3, choices are 2, 1, 3 and stable grouping
2904 // must visit rows 1, 0, 2.
2905 anchors: [3, 1, 0, 2],
2906 }
2907 }
2908
2909 fn value(tag: u8, index: u32) -> RawInline {
2910 let mut value = [0; 32];
2911 value[0] = tag;
2912 value[28..].copy_from_slice(&index.to_be_bytes());
2913 value
2914 }
2915
2916 fn count(&self, group: u8, variable: VariableId) -> u32 {
2917 match (group, variable) {
2918 (3 | 0, 1) => 0,
2919 (1, 2) => self.fanout,
2920 _ => 1,
2921 }
2922 }
2923 }
2924
2925 impl<'a> Constraint<'a> for Fixture {
2926 fn variables(&self) -> VariableSet {
2927 variable_set(0..4)
2928 }
2929
2930 fn estimate(&self, variable: VariableId, binding: &Binding) -> Option<usize> {
2931 let preferred = binding
2932 .get(0)
2933 .map(|anchor| match anchor[31] {
2934 0 => 1,
2935 1 => 2,
2936 2 => 3,
2937 _ => 1,
2938 })
2939 .unwrap_or(0);
2940 (variable < 4).then_some(if variable == 0 {
2941 4
2942 } else if variable == preferred {
2943 1
2944 } else {
2945 8
2946 })
2947 }
2948
2949 fn propose(
2950 &self,
2951 variable: VariableId,
2952 frontier: &Frontier<'_>,
2953 proposals: &mut ProposalBuffer,
2954 ) {
2955 for row in 0..frontier.len() {
2956 proposals.open(row as u32);
2957 if variable == 0 {
2958 proposals.extend(
2959 self.anchors
2960 .into_iter()
2961 .map(|group| Self::value(0, group as u32)),
2962 );
2963 } else {
2964 let group = frontier.row(row).get(0).expect("anchor bound")[31];
2965 proposals.extend(
2966 (0..self.count(group, variable))
2967 .map(|index| Self::value(variable as u8, index)),
2968 );
2969 }
2970 }
2971 }
2972
2973 fn confirm(
2974 &self,
2975 _variable: VariableId,
2976 _frontier: &Frontier<'_>,
2977 _candidates: &mut Candidates<'_>,
2978 ) {
2979 }
2980 }
2981
2982 type TestQuery = Query<Fixture, fn(&Binding<'_>) -> Option<RawInline>, RawInline>;
2983
2984 fn project(binding: &Binding<'_>) -> Option<RawInline> {
2985 binding.get(0).copied()
2986 }
2987
2988 fn query(fanout: u32) -> TestQuery {
2989 fixture_query(Fixture::identity_split(fanout))
2990 }
2991
2992 fn nonidentity_query(fanout: u32) -> TestQuery {
2993 fixture_query(Fixture::nonidentity_split(fanout))
2994 }
2995
2996 fn fixture_query(fixture: Fixture) -> TestQuery {
2997 Query::new(fixture, project as fn(&Binding<'_>) -> Option<RawInline>)
2998 .with_frontier_width(4)
2999 }
3000
3001 #[test]
3002 fn only_into_par_iter_sets_parallel_frontier_intent() {
3003 let pool = rayon::ThreadPoolBuilder::new()
3004 .num_threads(4)
3005 .build()
3006 .unwrap();
3007
3008 let sequential = Arc::new(AtomicU8::new(0));
3009 let sequential_rows = pool.install(|| {
3010 Query::new(IntentRecorder(Arc::clone(&sequential)), |_| Some(())).count()
3011 });
3012 assert_eq!(sequential_rows, 1);
3013 assert_eq!(
3014 sequential.load(Ordering::Relaxed),
3015 0b01,
3016 "an ordinary iterator stays serial even inside a Rayon pool"
3017 );
3018
3019 let parallel = Arc::new(AtomicU8::new(0));
3020 let parallel_rows = pool.install(|| {
3021 Query::new(IntentRecorder(Arc::clone(¶llel)), |_| Some(()))
3022 .into_par_iter()
3023 .count()
3024 });
3025 assert_eq!(parallel_rows, 1);
3026 assert_eq!(parallel.load(Ordering::Relaxed), 0b10);
3027 }
3028
3029 fn advance_to_three_groups(query: &mut TestQuery) {
3030 query.plan();
3031 query.next_group();
3032 query.next_chunk();
3033
3034 // Exhaust the empty one-row latency frontier, then widen the
3035 // root into three anchors with three preferred variables.
3036 query.plan();
3037 query.next_group();
3038 query.next_chunk();
3039 query.next_group();
3040 query.next_chunk();
3041 query.plan();
3042 assert_eq!(query.depths[query.depth].plan.group_count(), 3);
3043 assert_eq!(query.depths[query.depth].group, 0);
3044 assert_eq!(query.depths[query.depth].group_limit, 3);
3045 }
3046
3047 fn split_plan(query: &TestQuery) -> &SplitPlan {
3048 match &query.depths[query.depth].plan {
3049 DepthPlan::Split(plan) => plan,
3050 plan => panic!("expected split plan, got {plan:?}"),
3051 }
3052 }
3053
3054 #[test]
3055 fn universal_plan_stays_generated_without_choice_storage() {
3056 let mut representation = query(16);
3057 assert_eq!(representation.choice.capacity(), 0);
3058
3059 representation.plan();
3060
3061 assert!(matches!(
3062 representation.depths[0].plan,
3063 DepthPlan::Universal(0)
3064 ));
3065 assert_eq!(
3066 representation.depths[0].selection(),
3067 RowSelection::identity(1)
3068 );
3069 assert_eq!(
3070 representation.choice.capacity(),
3071 0,
3072 "unanimity must not allocate planner choice storage"
3073 );
3074 }
3075
3076 #[test]
3077 fn identity_partition_omits_the_row_order_override() {
3078 let mut representation = query(16);
3079 advance_to_three_groups(&mut representation);
3080
3081 let plan = split_plan(&representation);
3082 assert_eq!(plan.groups, [(1, 1), (2, 2), (3, 3)]);
3083 assert!(plan.order_override.is_empty());
3084 assert_eq!(
3085 representation.depths[representation.depth].selection(),
3086 RowSelection::identity(3)
3087 );
3088 }
3089
3090 #[test]
3091 fn nonidentity_partition_stores_only_the_row_order_override() {
3092 let mut representation = nonidentity_query(16);
3093 advance_to_three_groups(&mut representation);
3094
3095 let plan = split_plan(&representation);
3096 assert_eq!(plan.groups, [(1, 1), (2, 2), (3, 3)]);
3097 assert_eq!(plan.order_override, [1, 0, 2]);
3098 assert_eq!(
3099 representation.depths[representation.depth].selection(),
3100 RowSelection::explicit(&[1, 0, 2])
3101 );
3102
3103 let mut expected: Vec<_> = query(16).collect();
3104 let mut actual: Vec<_> = nonidentity_query(16).collect();
3105 expected.sort_unstable();
3106 actual.sort_unstable();
3107 assert_eq!(
3108 actual, expected,
3109 "row permutation must preserve the exact bag"
3110 );
3111 }
3112
3113 #[test]
3114 fn split_transfers_one_whole_group_and_fences_it() {
3115 const FANOUT: u32 = 64;
3116 let mut expected: Vec<_> = query(FANOUT).collect();
3117 expected.sort_unstable();
3118 assert_eq!(expected.len(), FANOUT as usize + 1);
3119
3120 let mut left = query(FANOUT);
3121 advance_to_three_groups(&mut left);
3122 let source_len = left.bindings.levels[0].buffer().len();
3123 let source_pos = left.bindings.levels[0].pos;
3124 let right = left.split_pending_group().expect("transfer first group");
3125
3126 assert_eq!(
3127 (right.depth, right.depths.len(), right.stack.len()),
3128 (0, 1, 0)
3129 );
3130 assert_eq!(
3131 (
3132 right.depths[0].group,
3133 right.depths[0].group_limit,
3134 right.depths[0].plan.group_count(),
3135 ),
3136 (0, 1, 3)
3137 );
3138 assert!(Arc::ptr_eq(&left.depths[1].block, &right.depths[0].block));
3139 assert_eq!(left.depths[1].selection(), right.depths[0].selection());
3140 match (&left.depths[1].plan, &right.depths[0].plan) {
3141 (DepthPlan::Split(left), DepthPlan::Split(right)) => {
3142 assert!(Arc::ptr_eq(left, right), "fencing must not clone the plan");
3143 }
3144 plans => panic!("expected shared split plans, got {plans:?}"),
3145 }
3146 assert_eq!(left.depths[1].group, 1, "left skips the owned group");
3147
3148 // The ancestor source was cloned intact, not bisected. Both
3149 // halves retain the same complete proposal region and cursor;
3150 // only their continuation ownership differs.
3151 assert_eq!(left.bindings.levels[0].buffer().len(), source_len);
3152 assert_eq!(right.bindings.levels[0].buffer().len(), source_len);
3153 assert_eq!(left.bindings.levels[0].pos, source_pos);
3154 assert_eq!(right.bindings.levels[0].pos, source_pos);
3155
3156 // A fenced sole root group is indivisible. Trying to split it
3157 // again must enter the work rather than hand the same group back.
3158 let mut fenced = right.clone();
3159 assert!(fenced.split_pending_group().is_none());
3160
3161 let second = left.split_pending_group().expect("transfer second group");
3162 assert!(
3163 left.split_pending_group().is_none(),
3164 "the last group stays with the only producer that can execute it"
3165 );
3166 assert_eq!(left.depths[1].group + 1, left.depths[1].group_limit);
3167
3168 let mut actual: Vec<_> = left.chain(right).chain(second).collect();
3169 actual.sort_unstable();
3170 assert_eq!(actual, expected, "splitting must preserve the exact bag");
3171 assert_eq!(actual.iter().filter(|row| row[31] == 2).count(), 1);
3172 }
3173
3174 fn advance_to_emit(query: &mut TestQuery) {
3175 loop {
3176 match query.mode {
3177 Search::Plan => query.plan(),
3178 Search::NextGroup => query.next_group(),
3179 Search::NextChunk => query.next_chunk(),
3180 Search::Emit => return,
3181 Search::Done => panic!("fixture ended before a terminal frontier"),
3182 }
3183 }
3184 }
3185
3186 #[test]
3187 fn terminal_page_is_transferred_without_slicing_rows() {
3188 let mut left = query(32);
3189 advance_to_emit(&mut left);
3190 assert!(left.depth > 0);
3191 let terminal_rows = left.depths[left.depth].rows;
3192 let terminal_block = Arc::clone(&left.depths[left.depth].block);
3193 let terminal_plan = left.depths[left.depth].plan.clone();
3194 let mut expected = left.clone().collect::<Vec<_>>();
3195
3196 let right = left
3197 .split_terminal_frontier()
3198 .expect("terminal page has a parent continuation");
3199 assert_eq!(right.depth, 0);
3200 assert_eq!(right.stack.len(), 0);
3201 assert_eq!(right.mode, Search::Emit);
3202 assert_eq!(right.depths[0].rows, terminal_rows);
3203 assert!(Arc::ptr_eq(&terminal_block, &right.depths[0].block));
3204 assert_eq!(
3205 terminal_plan.selection(terminal_rows),
3206 right.depths[0].selection()
3207 );
3208 for variable in left.bindings.bound {
3209 assert!(Arc::ptr_eq(
3210 left.bindings.levels[variable]
3211 .buffer
3212 .as_ref()
3213 .expect("bound level has a published buffer"),
3214 right.bindings.levels[variable]
3215 .buffer
3216 .as_ref()
3217 .expect("bound level has a published buffer"),
3218 ));
3219 }
3220
3221 let mut actual: Vec<_> = left.chain(right).collect();
3222 expected.sort_unstable();
3223 actual.sort_unstable();
3224 assert_eq!(actual, expected);
3225 }
3226
3227 #[test]
3228 fn indivisible_one_to_one_chain_never_splits_or_forces_copies() {
3229 const HOPS: usize = 60;
3230 let query = Query::new(OneToOne(HOPS), |binding: &Binding| {
3231 binding.get(HOPS - 1).copied()
3232 });
3233 let stats = query.stats();
3234
3235 // `UnindexedProducer::split` is allowed to drive the state
3236 // machine looking for a stealable unit. This chain has none:
3237 // every frontier is one sole group/page and every ancestor is
3238 // already spent. It must therefore stay one producer all the
3239 // way down rather than manufacture an empty left sibling at
3240 // each depth.
3241 let (producer, right) = query.into_par_iter().split();
3242 assert!(right.is_none());
3243 let rows = producer.collect::<Vec<_>>();
3244
3245 assert_eq!(rows, vec![Fixture::value(0x7f, HOPS as u32 - 1)]);
3246 assert_eq!(stats.inplace_descents(), HOPS as u64);
3247 assert_eq!(
3248 stats.copied_descents(),
3249 0,
3250 "a fruitless split search must not destroy Arc uniqueness"
3251 );
3252 }
3253
3254 #[test]
3255 fn repeated_manual_producer_splits_preserve_explicit_order_and_bag() {
3256 let pool = rayon::ThreadPoolBuilder::new()
3257 .num_threads(4)
3258 .build()
3259 .unwrap();
3260
3261 for fixture in [query as fn(u32) -> TestQuery, nonidentity_query] {
3262 let mut expected: Vec<_> = fixture(96).collect();
3263 expected.sort_unstable();
3264
3265 let mut queue = vec![fixture(96).into_par_iter()];
3266 let mut leaves = Vec::new();
3267 for _ in 0..64 {
3268 let Some(producer) = queue.pop() else {
3269 break;
3270 };
3271 let (left, right) = producer.split();
3272 if let Some(right) = right {
3273 queue.push(left);
3274 queue.push(right);
3275 } else {
3276 leaves.push(left);
3277 }
3278 }
3279
3280 let mut actual = Vec::new();
3281 for producer in queue.into_iter().chain(leaves) {
3282 actual.extend(producer.collect::<Vec<_>>());
3283 }
3284 actual.sort_unstable();
3285 assert_eq!(actual, expected);
3286
3287 for _ in 0..8 {
3288 let mut scheduled =
3289 pool.install(|| fixture(96).into_par_iter().collect::<Vec<_>>());
3290 scheduled.sort_unstable();
3291 assert_eq!(scheduled, expected);
3292 }
3293 }
3294 }
3295 }
3296}