Skip to main content

csp_solver/solver/gac/
mod.rs

1//! Unified GAC all-different core (Régin 1994), sentinel-generic and incremental.
2//!
3//! One propagator body serves both the plain all-different constraint and the
4//! sentinel-aware "all-different-except" variant. The distinction is a single
5//! [`Option<&D::Value>`] parameter:
6//!
7//! * `sentinel = None` — plain all-different. Every unassigned variable must be
8//!   matched to a distinct value.
9//! * `sentinel = Some(s)` — the value `s` is an "escape valve" any number of
10//!   variables may take; it is removed from the value side of the bipartite
11//!   graph, and variables whose domain has narrowed to `{s}` drop out of the
12//!   variable side.
13//!
14//! The Régin pipeline (Hopcroft-Karp maximum matching → residual graph → Tarjan
15//! SCC → prune edges outside every maximum matching) is identical for both; only
16//! participant/value collection and free-vertex reachability seeding branch on
17//! the sentinel. The graph primitives live in [`matching`].
18//!
19//! # Incrementality
20//!
21//! Two costs the from-scratch predecessor paid on *every* `revise()` are gone:
22//!
23//! 1. **Per-call heap allocation.** All working buffers (adjacency, matching,
24//!    residual graph, Tarjan stacks) live in a thread-local [`GacScratch`] that
25//!    is cleared — not freed — between calls. The value universe is the sole
26//!    per-call `Vec<V>` the generic bound cannot pool.
27//! 2. **Cold Hopcroft-Karp.** With a stable constraint id, the previous maximum
28//!    matching is cached per constraint and warm-starts the next call: cached
29//!    edges still valid against the live domains seed `match_u`/`match_v`, and
30//!    Hopcroft-Karp only augments from the vertices left free — O(E) repair
31//!    rather than O(E·√V) reconstruction. The cache is a pure hint: every seeded
32//!    edge is validated against live domains, so a stale cache costs extra
33//!    augmentation, never correctness.
34
35mod matching;
36
37use std::any::Any;
38use std::cell::RefCell;
39use std::collections::HashMap;
40use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
41
42use crate::constraint::traits::{Revision, VarId};
43use crate::domain::Domain;
44use crate::variable::Variable;
45
46use matching::{NONE, hopcroft_karp, reset_adj, tarjan_scc};
47
48/// Instrumentation: total entries into the unified GAC core (all variants).
49pub static GAC_CORE_CALLS: AtomicU64 = AtomicU64::new(0);
50
51/// Monotonic source of per-constraint cache ids.
52static NEXT_GAC_ID: AtomicU32 = AtomicU32::new(0);
53
54/// Allocate a fresh, process-unique constraint id for matching-cache keying.
55pub fn next_gac_id() -> u32 {
56    NEXT_GAC_ID.fetch_add(1, Ordering::Relaxed)
57}
58
59/// Below this live-participant count, GAC finds nothing singleton removal
60/// misses, so the caller's cheaper path is preferred. Exposed so constraints
61/// gate consistently.
62pub const GAC_MIN_PARTICIPANTS: usize = 3;
63
64/// A/B measurement toggle for the plain `AllDifferent` GAC path (Sudoku,
65/// Futoshiki). Default on; flipping it off reverts `AllDifferent` to
66/// singleton-removal-only so the pruning-strength delta can be measured.
67pub static GAC_IN_ALLDIFF_ENABLED: AtomicBool = AtomicBool::new(true);
68
69/// Cap on the per-thread matching cache; cleared wholesale past this many
70/// distinct constraints so a long-lived worker thread cannot leak unboundedly.
71const CACHE_CAP: usize = 8192;
72
73// ---------------------------------------------------------------------------
74// Reusable scratch
75// ---------------------------------------------------------------------------
76
77/// Per-thread, per-value-type reusable working set for the GAC core.
78///
79/// Generic in the value type only for the value universe and matching cache;
80/// every other buffer is integer-indexed and value-agnostic.
81struct GacScratch<V> {
82    participants: Vec<usize>,
83    has_sentinel: Vec<bool>,
84    assigned_ns: Vec<V>,
85    all_vals: Vec<V>,
86    adj: Vec<Vec<u32>>,
87    match_u: Vec<u32>,
88    match_v: Vec<u32>,
89    dist: Vec<u32>,
90    queue: Vec<u32>,
91    res_adj: Vec<Vec<u32>>,
92    reachable: Vec<bool>,
93    bfs: Vec<u32>,
94    t_index: Vec<u32>,
95    t_lowlink: Vec<u32>,
96    t_onstack: Vec<bool>,
97    t_scc: Vec<u32>,
98    t_stack: Vec<u32>,
99    t_call: Vec<(u32, u32)>,
100    cache: HashMap<u32, Vec<Option<V>>>,
101    /// Generation-stamped reverse map for the integer fast path (Beat 1).
102    ///
103    /// When `D::Value` is a small non-negative integer, `val_index[k]` holds
104    /// the `all_vals` slot for value `k` **iff** `val_index_gen[k] == cur_gen`.
105    /// The stamp makes the per-call reset O(1) (bump `cur_gen`) so the buffer
106    /// never leaks a prior call's numbering across a value-universe shrink —
107    /// see [`INV-A`](self). Non-integer / out-of-range values fall back to the
108    /// generic `PartialEq` `position` scan, so no `Hash`/`Ord`/`ValueIndex`
109    /// bound is introduced.
110    val_index: Vec<u32>,
111    val_index_gen: Vec<u32>,
112    cur_gen: u32,
113}
114
115/// Upper bound on the integer key the value→index fast path will address.
116/// A value above this (or negative, or non-integer) uses the `position`-scan
117/// fallback, bounding `val_index`'s worst-case footprint. Live GAC value
118/// universes are tiny (`BitsetDomain` is 0..128; assignment columns 0..n_cols),
119/// so the cap is never approached in practice.
120const MAX_FAST_INDEX: usize = 1 << 20;
121
122/// If `v` is one of the supported integer types and lands in
123/// `0..=MAX_FAST_INDEX`, return it as a reverse-map key; otherwise `None`
124/// (caller falls back to the `PartialEq` scan). Uses the `Any` blanket impl
125/// available to every `'static` type, so it adds **no** trait bound to the
126/// generic value — `FiniteDomain<String>` still routes entirely through the
127/// scan and compiles unchanged.
128fn fast_index<V: 'static>(v: &V) -> Option<usize> {
129    let any = v as &dyn Any;
130    macro_rules! try_ints {
131        ($($t:ty),*) => {$(
132            if let Some(&x) = any.downcast_ref::<$t>() {
133                return usize::try_from(x).ok().filter(|&k| k <= MAX_FAST_INDEX);
134            }
135        )*};
136    }
137    try_ints!(u8, u16, u32, u64, usize, i8, i16, i32, i64, isize);
138    None
139}
140
141impl<V> Default for GacScratch<V> {
142    fn default() -> Self {
143        Self {
144            participants: Vec::new(),
145            has_sentinel: Vec::new(),
146            assigned_ns: Vec::new(),
147            all_vals: Vec::new(),
148            adj: Vec::new(),
149            match_u: Vec::new(),
150            match_v: Vec::new(),
151            dist: Vec::new(),
152            queue: Vec::new(),
153            res_adj: Vec::new(),
154            reachable: Vec::new(),
155            bfs: Vec::new(),
156            t_index: Vec::new(),
157            t_lowlink: Vec::new(),
158            t_onstack: Vec::new(),
159            t_scc: Vec::new(),
160            t_stack: Vec::new(),
161            t_call: Vec::new(),
162            cache: HashMap::new(),
163            val_index: Vec::new(),
164            val_index_gen: Vec::new(),
165            cur_gen: 0,
166        }
167    }
168}
169
170thread_local! {
171    /// One type-erased [`GacScratch`] slot per thread. The concrete `V` for a
172    /// given solve is fixed, so the slot re-materializes only if a thread runs
173    /// solves over different value types.
174    static SCRATCH: RefCell<Box<dyn Any>> = RefCell::new(Box::new(()));
175}
176
177fn with_scratch<V, R>(f: impl FnOnce(&mut GacScratch<V>) -> R) -> R
178where
179    V: 'static,
180{
181    SCRATCH.with(|slot| {
182        let mut b = slot.borrow_mut();
183        if !b.is::<GacScratch<V>>() {
184            *b = Box::new(GacScratch::<V>::default());
185        }
186        let s = b.downcast_mut::<GacScratch<V>>().unwrap();
187        f(s)
188    })
189}
190
191// ---------------------------------------------------------------------------
192// Unified GAC core
193// ---------------------------------------------------------------------------
194
195/// Run Régin GAC on `scope`, sentinel-generic and incremental.
196///
197/// * `sentinel = None` → plain all-different; `Some(s)` → all-different-except.
198/// * `gac_id = Some(id)` enables the per-constraint matching warm-start cache;
199///   `None` runs stateless (used by the one-shot public wrappers and tests).
200///
201/// Only requires `D::Value: Clone + PartialEq + Debug` (all implied by `Domain`)
202/// plus `'static` for the thread-local scratch — no `Ord`, `Hash`, or
203/// `ValueIndex` bound, so `FiniteDomain<String>` still compiles.
204pub fn propagate_gac_core<D: Domain>(
205    scope: &[VarId],
206    sentinel: Option<&D::Value>,
207    variables: &mut [Variable<D>],
208    depth: usize,
209    gac_id: Option<u32>,
210) -> Revision
211where
212    D::Value: 'static,
213{
214    GAC_CORE_CALLS.fetch_add(1, Ordering::Relaxed);
215    with_scratch::<D::Value, _>(|s| propagate_inner(scope, sentinel, variables, depth, gac_id, s))
216}
217
218fn propagate_inner<D: Domain>(
219    scope: &[VarId],
220    sentinel: Option<&D::Value>,
221    variables: &mut [Variable<D>],
222    depth: usize,
223    gac_id: Option<u32>,
224    s: &mut GacScratch<D::Value>,
225) -> Revision
226where
227    D::Value: 'static,
228{
229    // Advance the value→index fast-path generation (Beat 1). Every stamped
230    // entry from a prior call is now stale in O(1); on the rare u32 wrap we
231    // clear the stamps once and restart at 1 (0 means "never written").
232    s.cur_gen = s.cur_gen.wrapping_add(1);
233    if s.cur_gen == 0 {
234        s.val_index_gen.iter_mut().for_each(|g| *g = 0);
235        s.cur_gen = 1;
236    }
237
238    // ----- Participant + assigned-value collection -----
239    s.assigned_ns.clear();
240    for &v in scope {
241        let dom = &variables[v as usize].domain;
242        if dom.is_empty() {
243            return Revision::Unsatisfiable;
244        }
245        if let Some(val) = dom.singleton_value()
246            && sentinel != Some(&val)
247        {
248            s.assigned_ns.push(val);
249        }
250    }
251
252    s.participants.clear();
253    s.has_sentinel.clear();
254    for (i, &v) in scope.iter().enumerate() {
255        let dom = &variables[v as usize].domain;
256        if dom.is_singleton() {
257            continue;
258        }
259        let dom_has_sentinel = sentinel.map(|sv| dom.contains(sv)).unwrap_or(false);
260        let non_sentinel_count = dom.size() - dom_has_sentinel as usize;
261        if non_sentinel_count == 0 {
262            // {sentinel}-only domain: committed to escape, drops out.
263            continue;
264        }
265        s.participants.push(i);
266        s.has_sentinel.push(dom_has_sentinel);
267    }
268
269    if s.participants.is_empty() {
270        return Revision::Unchanged;
271    }
272    let n_vars = s.participants.len();
273
274    // Plain all-different: below the threshold, singleton removal (which the
275    // caller performs) captures everything GAC would — skip the machinery.
276    if sentinel.is_none() && n_vars < GAC_MIN_PARTICIPANTS {
277        return Revision::Unchanged;
278    }
279
280    // ----- Value universe + bipartite adjacency -----
281    s.all_vals.clear();
282    reset_adj(&mut s.adj, n_vars);
283    for pu in 0..n_vars {
284        let var_id = scope[s.participants[pu]] as usize;
285        for val in variables[var_id].domain.iter() {
286            if sentinel == Some(&val) {
287                continue;
288            }
289            if s.assigned_ns.contains(&val) {
290                continue;
291            }
292            // Value→index resolution. Integer values in range take the O(1)
293            // generation-stamped reverse map; everything else falls back to
294            // the O(n_vals) `position` scan. Both dedup exactly (INV-B) and
295            // agree on the slot numbering, so the two paths are observably
296            // identical — the map is a pure accelerator over `all_vals`.
297            let vi = match fast_index(&val) {
298                Some(k) => {
299                    if k >= s.val_index.len() {
300                        s.val_index.resize(k + 1, 0);
301                        s.val_index_gen.resize(k + 1, 0);
302                    }
303                    if s.val_index_gen[k] == s.cur_gen {
304                        s.val_index[k]
305                    } else {
306                        let idx = s.all_vals.len() as u32;
307                        s.all_vals.push(val);
308                        s.val_index[k] = idx;
309                        s.val_index_gen[k] = s.cur_gen;
310                        idx
311                    }
312                }
313                None => match s.all_vals.iter().position(|x| *x == val) {
314                    Some(k) => k as u32,
315                    None => {
316                        s.all_vals.push(val);
317                        (s.all_vals.len() - 1) as u32
318                    }
319                },
320            };
321            s.adj[pu].push(vi);
322        }
323    }
324
325    // All non-sentinel values consumed by assigned singletons.
326    if s.all_vals.is_empty() {
327        return finish_all_consumed::<D>(scope, sentinel, variables, depth, s);
328    }
329    let n_vals = s.all_vals.len();
330
331    // ----- Warm-start matching from the per-constraint cache -----
332    s.match_u.clear();
333    s.match_u.resize(n_vars, NONE);
334    s.match_v.clear();
335    s.match_v.resize(n_vals, NONE);
336    s.dist.clear();
337    s.dist.resize(n_vars, 0);
338
339    if let Some(id) = gac_id
340        && let Some(cvec) = s.cache.get(&id)
341    {
342        for pu in 0..n_vars {
343            let Some(Some(cv)) = cvec.get(s.participants[pu]) else {
344                continue;
345            };
346            // Re-resolve the cached value against THIS call's `all_vals`
347            // (built just above, same generation), via the same fast path.
348            // A cached value not present in the current universe → skip, the
349            // warm start is a pure hint (INV-G).
350            let vi = match fast_index(cv) {
351                Some(k) if k < s.val_index.len() && s.val_index_gen[k] == s.cur_gen => {
352                    s.val_index[k]
353                }
354                Some(_) => continue,
355                None => match s.all_vals.iter().position(|x| x == cv) {
356                    Some(k) => k as u32,
357                    None => continue,
358                },
359            };
360            if s.match_v[vi as usize] == NONE && s.adj[pu].contains(&vi) {
361                s.match_u[pu] = vi;
362                s.match_v[vi as usize] = pu as u32;
363            }
364        }
365    }
366
367    hopcroft_karp(
368        n_vars,
369        n_vals,
370        &s.adj,
371        &mut s.match_u,
372        &mut s.match_v,
373        &mut s.dist,
374        &mut s.queue,
375    );
376
377    // ----- Coverage: sentinel-less participants must be matched -----
378    for pu in 0..n_vars {
379        if s.match_u[pu] == NONE && !s.has_sentinel[pu] {
380            return Revision::Unsatisfiable;
381        }
382    }
383
384    // Persist the fresh matching for the next call's warm start.
385    if let Some(id) = gac_id {
386        if s.cache.len() > CACHE_CAP {
387            s.cache.clear();
388        }
389        let cvec = s.cache.entry(id).or_default();
390        cvec.clear();
391        cvec.resize(scope.len(), None);
392        for pu in 0..n_vars {
393            if s.match_u[pu] != NONE {
394                cvec[s.participants[pu]] = Some(s.all_vals[s.match_u[pu] as usize].clone());
395            }
396        }
397    }
398
399    // ----- Residual graph -----
400    let total_nodes = n_vars + n_vals;
401    reset_adj(&mut s.res_adj, total_nodes);
402    for u in 0..n_vars {
403        let matched_vi = s.match_u[u];
404        for &vi in &s.adj[u] {
405            let val_node = (n_vars as u32) + vi;
406            if vi == matched_vi {
407                s.res_adj[val_node as usize].push(u as u32);
408            } else {
409                s.res_adj[u].push(val_node);
410            }
411        }
412    }
413
414    // ----- Reachability from free vertices -----
415    s.reachable.clear();
416    s.reachable.resize(total_nodes, false);
417    s.bfs.clear();
418    for vi in 0..n_vals {
419        if s.match_v[vi] == NONE {
420            let node = n_vars + vi;
421            s.reachable[node] = true;
422            s.bfs.push(node as u32);
423        }
424    }
425    for pu in 0..n_vars {
426        if s.match_u[pu] == NONE && s.has_sentinel[pu] {
427            s.reachable[pu] = true;
428            s.bfs.push(pu as u32);
429        }
430    }
431    let mut head = 0;
432    while head < s.bfs.len() {
433        let node = s.bfs[head] as usize;
434        head += 1;
435        for i in 0..s.res_adj[node].len() {
436            let next = s.res_adj[node][i] as usize;
437            if !s.reachable[next] {
438                s.reachable[next] = true;
439                s.bfs.push(next as u32);
440            }
441        }
442    }
443
444    // ----- SCCs -----
445    resize_tarjan(s, total_nodes);
446    tarjan_scc(
447        total_nodes,
448        &s.res_adj,
449        &mut s.t_index,
450        &mut s.t_lowlink,
451        &mut s.t_onstack,
452        &mut s.t_scc,
453        &mut s.t_stack,
454        &mut s.t_call,
455    );
456
457    // ----- Prune -----
458    let mut changed = false;
459
460    // Phase 1: assigned-singleton non-sentinel values were excluded from the
461    // graph; remove them from every participant's live domain.
462    if !s.assigned_ns.is_empty() {
463        for pu in 0..n_vars {
464            let var_id = scope[s.participants[pu]] as usize;
465            for k in 0..s.assigned_ns.len() {
466                let val = s.assigned_ns[k].clone();
467                if variables[var_id].prune(&val, depth) {
468                    changed = true;
469                }
470            }
471            if variables[var_id].domain.is_empty() {
472                return Revision::Unsatisfiable;
473            }
474        }
475    }
476
477    // Phase 2: Régin SCC pruning — drop unmatched (var, val) edges crossing SCC
478    // boundaries and not reachable from a free vertex.
479    for pu in 0..n_vars {
480        let var_id = scope[s.participants[pu]] as usize;
481        let matched_vi = s.match_u[pu];
482        for i in 0..s.adj[pu].len() {
483            let vi = s.adj[pu][i];
484            if vi == matched_vi {
485                continue;
486            }
487            let val_node = n_vars + vi as usize;
488            if s.t_scc[pu] == s.t_scc[val_node] || s.reachable[val_node] {
489                continue;
490            }
491            let val = s.all_vals[vi as usize].clone();
492            if variables[var_id].prune(&val, depth) {
493                changed = true;
494            }
495        }
496        if variables[var_id].domain.is_empty() {
497            return Revision::Unsatisfiable;
498        }
499    }
500
501    if changed {
502        Revision::Changed
503    } else {
504        Revision::Unchanged
505    }
506}
507
508/// Handle the degenerate case where the value universe is empty (every
509/// non-sentinel value was consumed by an assigned singleton).
510fn finish_all_consumed<D: Domain>(
511    scope: &[VarId],
512    sentinel: Option<&D::Value>,
513    variables: &mut [Variable<D>],
514    depth: usize,
515    s: &mut GacScratch<D::Value>,
516) -> Revision {
517    // Plain variant: an unassigned variable with no available value is UNSAT.
518    if sentinel.is_none() {
519        return Revision::Unsatisfiable;
520    }
521    // Sentinel variant: participants survive only via the escape valve. Any
522    // participant without a sentinel is unsatisfiable; the rest have their
523    // (now-forbidden) assigned values pruned.
524    for pu in 0..s.participants.len() {
525        if !s.has_sentinel[pu] {
526            return Revision::Unsatisfiable;
527        }
528    }
529    let mut changed = false;
530    for pu in 0..s.participants.len() {
531        let var_id = scope[s.participants[pu]] as usize;
532        for k in 0..s.assigned_ns.len() {
533            let val = s.assigned_ns[k].clone();
534            if variables[var_id].prune(&val, depth) {
535                changed = true;
536            }
537        }
538        if variables[var_id].domain.is_empty() {
539            return Revision::Unsatisfiable;
540        }
541    }
542    if changed {
543        Revision::Changed
544    } else {
545        Revision::Unchanged
546    }
547}
548
549fn resize_tarjan<V>(s: &mut GacScratch<V>, n: usize) {
550    s.t_index.resize(n, NONE);
551    s.t_lowlink.resize(n, 0);
552    s.t_onstack.clear();
553    s.t_onstack.resize(n, false);
554    s.t_scc.resize(n, NONE);
555}