Skip to main content

triblespace_core/query/
unionconstraint.rs

1use std::mem;
2
3use super::*;
4use itertools::Itertools;
5
6/// Logical disjunction of constraints (OR).
7///
8/// A value is accepted if *any* variant accepts it. Built by the
9/// [`or!`](crate::or) macro, by [`pattern_changes!`](crate::pattern_changes),
10/// or directly via [`new`](Self::new).
11///
12/// All variants must declare the same [`VariableSet`]; this is asserted at
13/// construction time. Estimates are summed across variants, proposals are
14/// merged and deduplicated, and confirmations are unioned via
15/// [`kmerge`](itertools::Itertools::kmerge).
16///
17/// Before proposing or confirming, the union checks each variant's
18/// [`satisfied`](Constraint::satisfied) status and skips variants that are
19/// provably dead. This prevents a value confirmed by a dead variant from
20/// leaking into the result set — the fix for spurious results in
21/// multi-entity [`pattern_changes!`](crate::pattern_changes) joins.
22pub struct UnionConstraint<C> {
23    constraints: Vec<C>,
24}
25
26impl<'a, C> UnionConstraint<C>
27where
28    C: Constraint<'a> + 'a,
29{
30    /// Creates a union over the given constraints.
31    ///
32    /// # Panics
33    ///
34    /// Panics if the variants do not all declare the same variable set.
35    pub fn new(constraints: Vec<C>) -> Self {
36        assert!(constraints
37            .iter()
38            .map(|c| c.variables())
39            .tuple_windows()
40            .all(|(a, b)| a == b));
41        UnionConstraint { constraints }
42    }
43}
44
45impl<'a, C> Constraint<'a> for UnionConstraint<C>
46where
47    C: Constraint<'a> + 'a,
48{
49    /// Returns the variable set of the first variant (all variants share
50    /// the same set, enforced at construction).
51    fn variables(&self) -> VariableSet {
52        self.constraints[0].variables()
53    }
54
55    /// Returns the **sum** of estimates across all variants. A union can
56    /// produce candidates from any branch, so the cardinalities add.
57    fn estimate(&self, variable: VariableId, binding: &Binding) -> Option<usize> {
58        self.constraints
59            .iter()
60            .filter_map(|c| c.estimate(variable, binding))
61            .reduce(|acc, e| acc + e)
62    }
63
64    /// Collects proposals from every *satisfied* variant, then sorts and
65    /// deduplicates. Dead variants (where [`satisfied`](Constraint::satisfied)
66    /// returns `false`) are skipped so their stale bindings cannot inject
67    /// values that no live variant would produce.
68    fn propose(&self, variable: VariableId, binding: &Binding, proposals: &mut Vec<RawValue>) {
69        self.constraints
70            .iter()
71            .filter(|c| c.satisfied(binding))
72            .for_each(|c| c.propose(variable, binding, proposals));
73        proposals.sort_unstable();
74        proposals.dedup();
75    }
76
77    /// Confirms proposals against every *satisfied* variant independently,
78    /// then merges the per-variant survivors via
79    /// [`kmerge`](itertools::Itertools::kmerge) and deduplicates. A value
80    /// passes if *any* live variant confirms it.
81    fn confirm(&self, variable: VariableId, binding: &Binding, proposals: &mut Vec<RawValue>) {
82        proposals.sort_unstable();
83
84        let union: Vec<_> = self
85            .constraints
86            .iter()
87            .filter(|c| c.satisfied(binding))
88            .map(|c| {
89                let mut proposals = proposals.clone();
90                c.confirm(variable, binding, &mut proposals);
91                proposals
92            })
93            .kmerge()
94            .dedup()
95            .collect();
96
97        _ = mem::replace(proposals, union);
98    }
99
100    /// Returns `true` when **at least one** variant is satisfied.
101    fn satisfied(&self, binding: &Binding) -> bool {
102        self.constraints.iter().any(|c| c.satisfied(binding))
103    }
104
105    /// Returns the union of all variants' influence sets for `variable`.
106    fn influence(&self, variable: VariableId) -> VariableSet {
107        self.constraints
108            .iter()
109            .fold(VariableSet::new_empty(), |acc, c| {
110                acc.union(c.influence(variable))
111            })
112    }
113}
114
115/// Combines constraints into a [`UnionConstraint`] (logical OR).
116///
117/// A result is produced when *any* of the given constraints is satisfied.
118/// All constraints must declare the same variable set.
119///
120/// ```rust,ignore
121/// or!(pattern!(&set_a, [...]), pattern!(&set_b, [...]))
122/// ```
123#[macro_export]
124macro_rules! or {
125    ($($c:expr),+ $(,)?) => (
126        $crate::query::unionconstraint::UnionConstraint::new(vec![
127            $(Box::new($c) as Box<dyn $crate::query::Constraint>),+
128        ])
129    )
130}
131
132/// Re-export of the [`or!`] macro.
133pub use or;