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;