triblespace-core 0.35.0

The triblespace core implementation.
use std::mem;

use super::*;
use itertools::Itertools;

/// Logical disjunction of constraints (OR).
///
/// A value is accepted if *any* variant accepts it. Built by the
/// [`or!`](crate::or) macro, by [`pattern_changes!`](crate::pattern_changes),
/// or directly via [`new`](Self::new).
///
/// All variants must declare the same [`VariableSet`]; this is asserted at
/// construction time. Estimates are summed across variants, proposals are
/// merged and deduplicated, and confirmations are unioned via
/// [`kmerge`](itertools::Itertools::kmerge).
///
/// Before proposing or confirming, the union checks each variant's
/// [`satisfied`](Constraint::satisfied) status and skips variants that are
/// provably dead. This prevents a value confirmed by a dead variant from
/// leaking into the result set — the fix for spurious results in
/// multi-entity [`pattern_changes!`](crate::pattern_changes) joins.
pub struct UnionConstraint<C> {
    constraints: Vec<C>,
}

impl<'a, C> UnionConstraint<C>
where
    C: Constraint<'a> + 'a,
{
    /// Creates a union over the given constraints.
    ///
    /// # Panics
    ///
    /// Panics if the variants do not all declare the same variable set.
    pub fn new(constraints: Vec<C>) -> Self {
        assert!(constraints
            .iter()
            .map(|c| c.variables())
            .tuple_windows()
            .all(|(a, b)| a == b));
        UnionConstraint { constraints }
    }
}

impl<'a, C> Constraint<'a> for UnionConstraint<C>
where
    C: Constraint<'a> + 'a,
{
    /// Returns the variable set of the first variant (all variants share
    /// the same set, enforced at construction).
    fn variables(&self) -> VariableSet {
        self.constraints[0].variables()
    }

    /// Returns the **sum** of estimates across all variants. A union can
    /// produce candidates from any branch, so the cardinalities add.
    fn estimate(&self, variable: VariableId, binding: &Binding) -> Option<usize> {
        self.constraints
            .iter()
            .filter_map(|c| c.estimate(variable, binding))
            .reduce(|acc, e| acc + e)
    }

    /// Collects proposals from every *satisfied* variant, then sorts and
    /// deduplicates. Dead variants (where [`satisfied`](Constraint::satisfied)
    /// returns `false`) are skipped so their stale bindings cannot inject
    /// values that no live variant would produce.
    fn propose(&self, variable: VariableId, binding: &Binding, proposals: &mut Vec<RawValue>) {
        self.constraints
            .iter()
            .filter(|c| c.satisfied(binding))
            .for_each(|c| c.propose(variable, binding, proposals));
        proposals.sort_unstable();
        proposals.dedup();
    }

    /// Confirms proposals against every *satisfied* variant independently,
    /// then merges the per-variant survivors via
    /// [`kmerge`](itertools::Itertools::kmerge) and deduplicates. A value
    /// passes if *any* live variant confirms it.
    fn confirm(&self, variable: VariableId, binding: &Binding, proposals: &mut Vec<RawValue>) {
        proposals.sort_unstable();

        let union: Vec<_> = self
            .constraints
            .iter()
            .filter(|c| c.satisfied(binding))
            .map(|c| {
                let mut proposals = proposals.clone();
                c.confirm(variable, binding, &mut proposals);
                proposals
            })
            .kmerge()
            .dedup()
            .collect();

        _ = mem::replace(proposals, union);
    }

    /// Returns `true` when **at least one** variant is satisfied.
    fn satisfied(&self, binding: &Binding) -> bool {
        self.constraints.iter().any(|c| c.satisfied(binding))
    }

    /// Returns the union of all variants' influence sets for `variable`.
    fn influence(&self, variable: VariableId) -> VariableSet {
        self.constraints
            .iter()
            .fold(VariableSet::new_empty(), |acc, c| {
                acc.union(c.influence(variable))
            })
    }
}

/// Combines constraints into a [`UnionConstraint`] (logical OR).
///
/// A result is produced when *any* of the given constraints is satisfied.
/// All constraints must declare the same variable set.
///
/// ```rust,ignore
/// or!(pattern!(&set_a, [...]), pattern!(&set_b, [...]))
/// ```
#[macro_export]
macro_rules! or {
    ($($c:expr),+ $(,)?) => (
        $crate::query::unionconstraint::UnionConstraint::new(vec![
            $(Box::new($c) as Box<dyn $crate::query::Constraint>),+
        ])
    )
}

/// Re-export of the [`or!`] macro.
pub use or;