Skip to main content

mesh_sieve/topology/
stack.rs

1//! Stack abstraction for vertical composition of Sieve topologies.
2//!
3//! A _Stack_ represents relationships ("vertical arrows") between two Sieves:
4//! a **base** mesh and a **cap** mesh (e.g., from elements to degrees-of-freedom).
5//! This module provides a generic `Stack` trait and an in-memory implementation,
6//! along with facilities for composing multiple stacks.
7//!
8//! [`Stack::add_arrow`] requires that `base` is present in the base sieve and `cap`
9//! is present in the cap sieve. Insert points first via the sieve mutators
10//! (`add_point`, `add_base_point`, `add_cap_point`). In debug and
11//! `strict-invariants` builds, violating this invariant panics; in all builds the
12//! method returns `Err(MeshSieveError::StackMissingPoint{..})` when either point
13//! is missing.
14
15use super::sieve::InMemorySieve;
16use crate::mesh_error::MeshSieveError;
17use crate::topology::_debug_invariants::{debug_invariants, inv_assert};
18use crate::topology::bounds::{PayloadLike, PointLike};
19use crate::topology::cache::InvalidateCache;
20use std::collections::HashMap;
21use std::sync::Arc;
22
23/// A `Stack` links a *base* Sieve to a *cap* Sieve via vertical arrows.
24/// Each vertical arrow carries its own payload (e.g., polarity or permutation),
25/// independent of the horizontal Sieve payloads.
26///
27/// - `Point`:            The point type in the base mesh (commonly `PointId`).
28/// - `CapPt`:            The point type in the cap mesh (commonly `PointId`).
29/// - `VerticalPayload`:  Data attached to each vertical arrow (e.g., `Polarity`).
30///
31/// Some implementations (such as [`ComposedStack`]) do not expose a concrete
32/// base or cap Sieve. Calling [`Stack::base`] or [`Stack::cap`] on such stacks
33/// will panic. A future refactor may return `Option` or `Result` to make these
34/// unsupported operations explicit.
35pub trait Stack {
36    /// Base mesh point identifier.
37    type Point: Copy + Eq + std::hash::Hash;
38    /// Cap mesh point identifier.
39    type CapPt: Copy + Eq + std::hash::Hash;
40    /// Vertical arrow payload type.
41    type VerticalPayload: Clone;
42    /// Underlying base Sieve type (horizontal), payload unconstrained.
43    type BaseSieve: crate::topology::sieve::sieve_trait::Sieve<Point = Self::Point>;
44    /// Underlying cap Sieve type (horizontal), payload unconstrained.
45    type CapSieve: crate::topology::sieve::sieve_trait::Sieve<Point = Self::CapPt>;
46
47    // === Topology queries ===
48    /// Returns an iterator over all upward arrows from base point `p` to cap points.
49    /// Each item is `(cap_point, vertical_payload)`.
50    fn lift<'a>(
51        &'a self,
52        p: Self::Point,
53    ) -> Box<dyn Iterator<Item = (Self::CapPt, Self::VerticalPayload)> + 'a>;
54
55    /// Returns an iterator over all downward arrows from cap point `q` to base points.
56    /// Each item is `(base_point, vertical_payload)`.
57    fn drop<'a>(
58        &'a self,
59        q: Self::CapPt,
60    ) -> Box<dyn Iterator<Item = (Self::Point, Self::VerticalPayload)> + 'a>;
61
62    // === Mutation helpers ===
63    /// Adds a new vertical arrow `base -> cap` with associated payload.
64    fn add_arrow(
65        &mut self,
66        base: Self::Point,
67        cap: Self::CapPt,
68        pay: Self::VerticalPayload,
69    ) -> Result<(), MeshSieveError>;
70
71    /// Removes the arrow `base -> cap`, returning its payload if present.
72    fn remove_arrow(
73        &mut self,
74        base: Self::Point,
75        cap: Self::CapPt,
76    ) -> Result<Option<Self::VerticalPayload>, MeshSieveError>;
77
78    // === Convenience accessors ===
79    /// Returns a reference to the underlying base Sieve.
80    ///
81    /// # Panics
82    /// Implementations may panic if the base Sieve is not exposed (e.g., [`ComposedStack`]).
83    fn base(&self) -> &Self::BaseSieve;
84    /// Returns a reference to the underlying cap Sieve.
85    ///
86    /// # Panics
87    /// Implementations may panic if the cap Sieve is not exposed (e.g., [`ComposedStack`]).
88    fn cap(&self) -> &Self::CapSieve;
89
90    /// Returns a mutable reference to the underlying base Sieve.
91    fn base_mut(&mut self) -> Result<&mut Self::BaseSieve, MeshSieveError>;
92    /// Returns a mutable reference to the underlying cap Sieve.
93    fn cap_mut(&mut self) -> Result<&mut Self::CapSieve, MeshSieveError>;
94}
95
96/// In-memory implementation of the `Stack` trait.
97///
98/// Stores vertical arrows in two hash maps:
99/// - `up`   maps base points to a list of `(cap_point, payload)`.
100/// - `down` maps cap points to a list of `(base_point, payload)`.
101///
102/// Also embeds two `InMemorySieve`s to represent the base and cap topologies themselves.
103#[derive(Clone, Debug)]
104pub struct InMemoryStack<B: PointLike, C: PointLike, V = (), PB = (), PC = ()> {
105    /// Underlying base sieve (e.g., mesh connectivity).
106    base: InMemorySieve<B, PB>,
107    /// Underlying cap sieve (e.g., DOF connectivity).
108    cap: InMemorySieve<C, PC>,
109    /// Upward adjacency: base -> cap
110    pub up: HashMap<B, Vec<(C, V)>>,
111    /// Downward adjacency: cap -> base
112    pub down: HashMap<C, Vec<(B, V)>>,
113}
114
115impl<B, C, V, PB, PC> InMemoryStack<B, C, V, PB, PC>
116where
117    B: PointLike,
118    C: PointLike,
119{
120    /// Creates an empty `InMemoryStack` with no arrows.
121    pub fn new() -> Self {
122        Self {
123            base: InMemorySieve::default(),
124            cap: InMemorySieve::default(),
125            up: HashMap::new(),
126            down: HashMap::new(),
127        }
128    }
129
130    /// Hint: preallocate additional upward slots for `base`.
131    #[inline]
132    pub fn reserve_lift(&mut self, base: B, additional: usize) {
133        self.up.entry(base).or_default().reserve(additional);
134    }
135
136    /// Hint: preallocate additional downward slots for `cap`.
137    #[inline]
138    pub fn reserve_drop(&mut self, cap: C, additional: usize) {
139        self.down.entry(cap).or_default().reserve(additional);
140    }
141
142    /// Optionally release excess capacity after bulk construction.
143    pub fn shrink_to_fit(&mut self) {
144        for v in self.up.values_mut() {
145            v.shrink_to_fit();
146        }
147        for v in self.down.values_mut() {
148            v.shrink_to_fit();
149        }
150    }
151}
152
153/// Provides a default implementation for `InMemoryStack`.
154impl<B, C, V, PB, PC> Default for InMemoryStack<B, C, V, PB, PC>
155where
156    B: PointLike,
157    C: PointLike,
158    V: PayloadLike,
159{
160    fn default() -> Self {
161        Self {
162            base: InMemorySieve::default(),
163            cap: InMemorySieve::default(),
164            up: HashMap::new(),
165            down: HashMap::new(),
166        }
167    }
168}
169
170impl<B, C, V, PB, PC> Stack for InMemoryStack<B, C, V, PB, PC>
171where
172    B: PointLike,
173    C: PointLike,
174    V: PayloadLike,
175    PB: PayloadLike,
176    PC: PayloadLike,
177{
178    type Point = B;
179    type CapPt = C;
180    type VerticalPayload = V;
181    type BaseSieve = InMemorySieve<B, PB>;
182    type CapSieve = InMemorySieve<C, PC>;
183
184    fn lift<'a>(&'a self, p: B) -> Box<dyn Iterator<Item = (C, V)> + 'a> {
185        match self.up.get(&p) {
186            Some(vec) => Box::new(vec.iter().cloned()),
187            None => Box::new(std::iter::empty()),
188        }
189    }
190
191    fn drop<'a>(&'a self, q: C) -> Box<dyn Iterator<Item = (B, V)> + 'a> {
192        match self.down.get(&q) {
193            Some(vec) => Box::new(vec.iter().cloned()),
194            None => Box::new(std::iter::empty()),
195        }
196    }
197
198    fn add_arrow(&mut self, base: B, cap: C, pay: V) -> Result<(), MeshSieveError> {
199        if !self.base.contains_point(base) {
200            inv_assert!(false, "stack add_arrow: base point missing: {base:?}");
201            return Err(MeshSieveError::StackMissingPoint {
202                role: "base",
203                point: format!("{base:?}"),
204            });
205        }
206        if !self.cap.contains_point(cap) {
207            inv_assert!(false, "stack add_arrow: cap point missing: {cap:?}");
208            return Err(MeshSieveError::StackMissingPoint {
209                role: "cap",
210                point: format!("{cap:?}"),
211            });
212        }
213
214        let ups = self.up.entry(base).or_default();
215        if let Some((_, existing)) = ups.iter().find(|(c, _)| *c == cap) {
216            if *existing == pay {
217                return Ok(());
218            }
219            return Err(MeshSieveError::RelationConflict {
220                src: format!("{base:?}"),
221                dst: format!("{cap:?}"),
222                kind: crate::mesh_error::RelationConflictKind::Payload,
223            });
224        }
225        if let Some((_, existing)) = self
226            .down
227            .get(&cap)
228            .and_then(|v| v.iter().find(|(b, _)| *b == base))
229        {
230            if *existing != pay {
231                return Err(MeshSieveError::RelationConflict {
232                    src: format!("{base:?}"),
233                    dst: format!("{cap:?}"),
234                    kind: crate::mesh_error::RelationConflictKind::Payload,
235                });
236            }
237            // A matching mirror with a missing outgoing entry is repaired below.
238        }
239        ups.push((cap, pay.clone()));
240
241        let downs = self.down.entry(cap).or_default();
242        if let Some(slot) = downs.iter_mut().find(|(b, _)| *b == base) {
243            slot.1 = pay.clone();
244        } else {
245            downs.push((base, pay.clone()));
246        }
247
248        InvalidateCache::invalidate_cache(&mut self.base);
249        InvalidateCache::invalidate_cache(&mut self.cap);
250        debug_invariants!(self);
251        Ok(())
252    }
253
254    fn remove_arrow(&mut self, base: B, cap: C) -> Result<Option<V>, MeshSieveError> {
255        if !self.base.contains_point(base) {
256            inv_assert!(false, "stack remove_arrow: base point missing: {base:?}");
257        }
258        if !self.cap.contains_point(cap) {
259            inv_assert!(false, "stack remove_arrow: cap point missing: {cap:?}");
260        }
261
262        let mut removed = None;
263
264        let remove_up = if let Some(vec) = self.up.get_mut(&base) {
265            if let Some(pos) = vec.iter().position(|(c, _)| *c == cap) {
266                removed = Some(vec.remove(pos).1);
267            }
268            vec.is_empty()
269        } else {
270            false
271        };
272        if remove_up {
273            self.up.remove(&base);
274        }
275
276        let remove_down = if let Some(vec) = self.down.get_mut(&cap) {
277            if let Some(pos) = vec.iter().position(|(b, _)| *b == base) {
278                vec.remove(pos);
279            }
280            vec.is_empty()
281        } else {
282            false
283        };
284        if remove_down {
285            self.down.remove(&cap);
286        }
287        InvalidateCache::invalidate_cache(&mut self.base);
288        InvalidateCache::invalidate_cache(&mut self.cap);
289        debug_invariants!(self);
290        Ok(removed)
291    }
292
293    fn base(&self) -> &Self::BaseSieve {
294        &self.base
295    }
296    fn cap(&self) -> &Self::CapSieve {
297        &self.cap
298    }
299    fn base_mut(&mut self) -> Result<&mut Self::BaseSieve, MeshSieveError> {
300        Ok(&mut self.base)
301    }
302    fn cap_mut(&mut self) -> Result<&mut Self::CapSieve, MeshSieveError> {
303        Ok(&mut self.cap)
304    }
305}
306
307impl<B, C, T, PB, PC> InMemoryStack<B, C, Arc<T>, PB, PC>
308where
309    B: PointLike,
310    C: PointLike,
311    T: PartialEq,
312    PB: PayloadLike,
313    PC: PayloadLike,
314{
315    #[inline]
316    pub fn add_arrow_val(&mut self, base: B, cap: C, payload: T) -> Result<(), MeshSieveError> {
317        self.add_arrow(base, cap, Arc::new(payload))
318    }
319}
320
321/// Provides accessors for base and cap points for testability.
322impl<B, C, V, PB, PC> InMemoryStack<B, C, V, PB, PC>
323where
324    B: PointLike,
325    C: PointLike,
326    V: PayloadLike,
327{
328    pub fn base_points(&self) -> impl Iterator<Item = B> + '_ {
329        self.up.keys().copied()
330    }
331    pub fn cap_points(&self) -> impl Iterator<Item = C> + '_ {
332        self.down.keys().copied()
333    }
334    #[cfg(any(debug_assertions, feature = "strict-invariants"))]
335    pub(crate) fn debug_assert_invariants(&self) {
336        use std::collections::HashSet;
337
338        for (b, v) in &self.up {
339            let mut seen = HashSet::new();
340            for (c, _) in v {
341                crate::topology::_debug_invariants::inv_assert!(
342                    seen.insert(*c),
343                    "duplicate vertical arrow base={b:?} cap={c:?}"
344                );
345            }
346        }
347        for (c, v) in &self.down {
348            let mut seen = HashSet::new();
349            for (b, _) in v {
350                crate::topology::_debug_invariants::inv_assert!(
351                    seen.insert(*b),
352                    "duplicate vertical arrow cap={c:?} base={b:?}"
353                );
354            }
355        }
356
357        let out_total: usize = self.up.values().map(|v| v.len()).sum();
358        let in_total: usize = self.down.values().map(|v| v.len()).sum();
359        crate::topology::_debug_invariants::inv_assert_eq!(
360            out_total,
361            in_total,
362            "stack up/down totals differ",
363        );
364
365        for (b, ups) in &self.up {
366            crate::topology::_debug_invariants::inv_assert!(
367                self.base.adjacency_out.contains_key(b) || self.base.adjacency_in.contains_key(b),
368                "vertical base point {b:?} not present in base sieve"
369            );
370            for (c, _) in ups {
371                crate::topology::_debug_invariants::inv_assert!(
372                    self.cap.adjacency_out.contains_key(c) || self.cap.adjacency_in.contains_key(c),
373                    "vertical cap point {c:?} not present in cap sieve"
374                );
375                let has = self
376                    .down
377                    .get(c)
378                    .is_some_and(|v| v.iter().any(|(bb, _)| *bb == *b));
379                crate::topology::_debug_invariants::inv_assert!(
380                    has,
381                    "stack mirror missing: up {b:?}->{c:?} has no down",
382                );
383            }
384        }
385        for (c, downs) in &self.down {
386            crate::topology::_debug_invariants::inv_assert!(
387                self.cap.adjacency_out.contains_key(c) || self.cap.adjacency_in.contains_key(c),
388                "vertical cap point {c:?} not present in cap sieve"
389            );
390            for (b, _) in downs {
391                crate::topology::_debug_invariants::inv_assert!(
392                    self.base.adjacency_out.contains_key(b)
393                        || self.base.adjacency_in.contains_key(b),
394                    "vertical base point {b:?} not present in base sieve"
395                );
396                let has = self
397                    .up
398                    .get(b)
399                    .is_some_and(|v| v.iter().any(|(cc, _)| *cc == *c));
400                crate::topology::_debug_invariants::inv_assert!(
401                    has,
402                    "stack mirror missing: down {b:?}->{c:?} has no up",
403                );
404            }
405        }
406    }
407}
408
409/// A stack composed of two existing stacks: `lower: base -> mid` and `upper: mid -> cap`.
410///
411/// Traversal composes payloads via a `compose_payload` function.
412///
413/// # Examples
414///
415/// Using [`Polarity`] payloads (XOR composition):
416/// ```
417/// use mesh_sieve::topology::arrow::Polarity;
418/// use mesh_sieve::topology::stack::{ComposedStack, InMemoryStack, Stack};
419/// use mesh_sieve::topology::point::PointId;
420/// let s1 = InMemoryStack::<PointId, PointId, Polarity>::new();
421/// let s2 = InMemoryStack::<PointId, PointId, Polarity>::new();
422/// let _cs = ComposedStack::new(&s1, &s2, |a, b| (*a) ^ (*b));
423/// ```
424///
425/// Using group-valued [`orientation::Sign`] with trait composition:
426/// ```
427/// use mesh_sieve::topology::orientation::Sign;
428/// use mesh_sieve::topology::sieve::oriented::Orientation as _;
429/// use mesh_sieve::topology::stack::{ComposedStack, InMemoryStack, Stack};
430/// use mesh_sieve::topology::point::PointId;
431/// let s1 = InMemoryStack::<PointId, PointId, Sign>::new();
432/// let s2 = InMemoryStack::<PointId, PointId, Sign>::new();
433/// let _cs = ComposedStack::new(&s1, &s2, |a, b| Sign::compose(*a, *b));
434/// ```
435pub struct ComposedStack<'a, S1, S2, F, VO>
436where
437    S1: Stack,
438    S2: Stack<Point = S1::CapPt>,
439    F: Fn(&S1::VerticalPayload, &S2::VerticalPayload) -> VO,
440    VO: Clone,
441{
442    pub lower: &'a S1,
443    pub upper: &'a S2,
444    pub compose_payload: F,
445    _phantom: core::marker::PhantomData<VO>,
446}
447
448impl<'a, S1, S2, F, VO> ComposedStack<'a, S1, S2, F, VO>
449where
450    S1: Stack,
451    S2: Stack<Point = S1::CapPt>,
452    F: Fn(&S1::VerticalPayload, &S2::VerticalPayload) -> VO,
453    VO: Clone,
454{
455    pub fn new(lower: &'a S1, upper: &'a S2, compose_payload: F) -> Self {
456        Self {
457            lower,
458            upper,
459            compose_payload,
460            _phantom: core::marker::PhantomData,
461        }
462    }
463}
464
465impl<'a, S1, S2, F, VO> Stack for ComposedStack<'a, S1, S2, F, VO>
466where
467    S1: Stack,
468    S2: Stack<Point = S1::CapPt>,
469    F: Fn(&S1::VerticalPayload, &S2::VerticalPayload) -> VO + Sync + Send,
470    VO: Clone,
471{
472    type Point = S1::Point;
473    type CapPt = S2::CapPt;
474    type VerticalPayload = VO;
475    type BaseSieve = S1::BaseSieve;
476    type CapSieve = S2::CapSieve;
477
478    fn lift<'b>(&'b self, p: S1::Point) -> Box<dyn Iterator<Item = (S2::CapPt, VO)> + 'b> {
479        let lower = self.lower.lift(p);
480        let iter = lower.flat_map(move |(mid, pay1)| {
481            self.upper
482                .lift(mid)
483                .map(move |(cap, pay2)| (cap, (self.compose_payload)(&pay1, &pay2)))
484        });
485        Box::new(iter)
486    }
487
488    fn drop<'b>(&'b self, q: S2::CapPt) -> Box<dyn Iterator<Item = (S1::Point, VO)> + 'b> {
489        let upper = self.upper.drop(q);
490        let iter = upper.flat_map(move |(mid, pay2)| {
491            self.lower
492                .drop(mid)
493                .map(move |(base, pay1)| (base, (self.compose_payload)(&pay1, &pay2)))
494        });
495        Box::new(iter)
496    }
497
498    fn add_arrow(
499        &mut self,
500        _base: S1::Point,
501        _cap: S2::CapPt,
502        _pay: VO,
503    ) -> Result<(), MeshSieveError> {
504        Err(MeshSieveError::UnsupportedStackOperation(
505            "add_arrow on ComposedStack",
506        ))
507    }
508    fn remove_arrow(
509        &mut self,
510        _base: S1::Point,
511        _cap: S2::CapPt,
512    ) -> Result<Option<VO>, MeshSieveError> {
513        Err(MeshSieveError::UnsupportedStackOperation(
514            "remove_arrow on ComposedStack",
515        ))
516    }
517    fn base_mut(&mut self) -> Result<&mut Self::BaseSieve, MeshSieveError> {
518        Err(MeshSieveError::UnsupportedStackOperation(
519            "base_mut on ComposedStack",
520        ))
521    }
522    fn cap_mut(&mut self) -> Result<&mut Self::CapSieve, MeshSieveError> {
523        Err(MeshSieveError::UnsupportedStackOperation(
524            "cap_mut on ComposedStack",
525        ))
526    }
527    /// Returns the base sieve of the composed stack.
528    ///
529    /// # Panics
530    /// Panics because `ComposedStack` does not have direct access to a base sieve.
531    fn base(&self) -> &Self::BaseSieve {
532        panic!("base() is not supported on ComposedStack")
533    }
534    /// Returns the cap sieve of the composed stack.
535    ///
536    /// # Panics
537    /// Panics because `ComposedStack` does not have direct access to a cap sieve.
538    fn cap(&self) -> &Self::CapSieve {
539        panic!("cap() is not supported on ComposedStack")
540    }
541}
542
543#[test]
544fn composed_stack_no_leak() {
545    // This test is no longer needed: buffer reuse is gone, and all payloads are owned.
546}
547
548impl<B, C, V, PB, PC> InvalidateCache for InMemoryStack<B, C, V, PB, PC>
549where
550    B: PointLike,
551    C: PointLike,
552    V: PayloadLike,
553{
554    fn invalidate_cache(&mut self) {
555        // stack-local caches only; base and cap left untouched
556    }
557}
558
559impl<B, C, V, PB, PC> InMemoryStack<B, C, V, PB, PC>
560where
561    B: PointLike,
562    C: PointLike,
563    PB: PayloadLike,
564    PC: PayloadLike,
565{
566    #[inline]
567    pub fn invalidate_base_and_cap(&mut self) {
568        self.base.invalidate_cache();
569        self.cap.invalidate_cache();
570    }
571}
572
573#[cfg(test)]
574mod tests {
575    use super::*;
576    use crate::topology::sieve::sieve_trait::Sieve;
577    #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, PartialOrd, Ord)]
578    struct V(u32);
579    #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, PartialOrd, Ord)]
580    struct Dof(u32);
581
582    #[test]
583    fn stack_payloads_decoupled() {
584        // Base payload u8, Cap payload String, Vertical payload bool
585        type S = InMemoryStack<u32, u32, bool, u8, String>;
586        let mut st = S::default();
587        st.base.add_arrow(1, 2, 7u8);
588        st.cap.add_arrow(10, 20, "x".to_string());
589        st.add_arrow(1, 10, true).unwrap();
590        let v: Vec<_> = st.lift(1).collect();
591        assert_eq!(v, vec![(10, true)]);
592    }
593    #[test]
594    fn add_and_lift_drop() {
595        let mut stack = InMemoryStack::<V, Dof, i32>::new();
596        stack.base.add_arrow(V(1), V(1), ());
597        stack.cap.add_arrow(Dof(10), Dof(10), ());
598        stack.cap.add_arrow(Dof(11), Dof(11), ());
599        let _ = stack.add_arrow(V(1), Dof(10), 42);
600        let _ = stack.add_arrow(V(1), Dof(11), 43);
601        let mut lifted: Vec<_> = stack.lift(V(1)).collect();
602        lifted.sort_by_key(|(dof, _)| dof.0);
603        assert_eq!(lifted, vec![(Dof(10), 42), (Dof(11), 43)]);
604        let dropped: Vec<_> = stack.drop(Dof(10)).collect();
605        assert_eq!(dropped, vec![(V(1), 42)]);
606    }
607    #[test]
608    fn remove_arrow_behavior() {
609        let mut stack = InMemoryStack::<V, Dof, i32>::new();
610        stack.base.add_arrow(V(1), V(1), ());
611        stack.cap.add_arrow(Dof(10), Dof(10), ());
612        let _ = stack.add_arrow(V(1), Dof(10), 99);
613        assert_eq!(stack.remove_arrow(V(1), Dof(10)).unwrap(), Some(99));
614        // Double remove returns None
615        assert_eq!(stack.remove_arrow(V(1), Dof(10)).unwrap(), None);
616        // After removal, lift/drop are empty
617        assert!(stack.lift(V(1)).next().is_none());
618        assert!(stack.drop(Dof(10)).next().is_none());
619    }
620
621    #[test]
622    fn composed_stack_lift_drop() {
623        use super::*;
624        #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, PartialOrd, Ord)]
625        struct A(u32);
626        #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, PartialOrd, Ord)]
627        struct B(u32);
628        #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, PartialOrd, Ord)]
629        struct C(u32);
630        // Stack1: A → B
631        let mut s1 = InMemoryStack::<A, B, i32>::new();
632        s1.base.add_arrow(A(1), A(1), ());
633        s1.cap.add_arrow(B(10), B(10), ());
634        s1.cap.add_arrow(B(11), B(11), ());
635        let _ = s1.add_arrow(A(1), B(10), 2);
636        let _ = s1.add_arrow(A(1), B(11), 3);
637        // Stack2: B → C
638        let mut s2 = InMemoryStack::<B, C, i32>::new();
639        s2.base.add_arrow(B(10), B(10), ());
640        s2.base.add_arrow(B(11), B(11), ());
641        s2.cap.add_arrow(C(100), C(100), ());
642        s2.cap.add_arrow(C(101), C(101), ());
643        let _ = s2.add_arrow(B(10), C(100), 5);
644        let _ = s2.add_arrow(B(11), C(101), 7);
645        // Compose: payloads are summed
646        let composed = ComposedStack::new(&s1, &s2, |p1, p2| p1 + p2);
647        let mut lifted: Vec<_> = composed.lift(A(1)).collect();
648        lifted.sort_by_key(|(c, _)| (*c).0);
649        assert_eq!(lifted, vec![(C(100), 7), (C(101), 10)]);
650        let dropped: Vec<_> = composed.drop(C(100)).collect();
651        assert_eq!(dropped, vec![(A(1), 7)]);
652    }
653
654    #[test]
655    fn vertical_edits_invalidate_horizontal_caches() {
656        let mut s = InMemoryStack::<u32, u32, (), (), ()>::default();
657        s.base.add_arrow(1, 2, ());
658        s.cap.add_arrow(10, 20, ());
659        assert!(s.base.strata.get().is_none());
660        assert!(s.cap.strata.get().is_none());
661        let _ = s.base.chart_points();
662        let _ = s.cap.chart_points();
663        assert!(s.base.strata.get().is_some());
664        assert!(s.cap.strata.get().is_some());
665        s.add_arrow(1, 10, ()).unwrap();
666        assert!(s.base.strata.get().is_none());
667        assert!(s.cap.strata.get().is_none());
668    }
669
670    #[test]
671    fn base_and_cap_points_reflect_maps() {
672        let mut s = InMemoryStack::<u32, u32, ()>::new();
673        s.base.add_arrow(1, 1, ());
674        s.cap.add_arrow(10, 10, ());
675        // empty
676        assert!(s.base_points().next().is_none());
677        assert!(s.cap_points().next().is_none());
678        // add arrow 1→10
679        let _ = s.add_arrow(1, 10, ());
680        let bases: Vec<_> = s.base_points().collect();
681        let caps: Vec<_> = s.cap_points().collect();
682        assert_eq!(bases, vec![1]);
683        assert_eq!(caps, vec![10]);
684    }
685
686    #[test]
687    fn new_and_default_empty() {
688        let s1 = InMemoryStack::<u8, u8, u8>::new();
689        let s2: InMemoryStack<u8, u8, u8> = Default::default();
690        assert_eq!(s1.base_points().count(), 0);
691        assert_eq!(s2.cap_points().count(), 0);
692    }
693
694    #[test]
695    fn composed_stack_add_arrow_error() {
696        let s1 = InMemoryStack::<u8, u8, u8>::new();
697        let s2 = InMemoryStack::<u8, u8, u8>::new();
698        let mut cs = ComposedStack::new(&s1, &s2, |a, _b| *a);
699        let err = cs.add_arrow(0, 0, 0).unwrap_err();
700        assert_eq!(
701            err.to_string(),
702            MeshSieveError::UnsupportedStackOperation("add_arrow on ComposedStack").to_string()
703        );
704    }
705    #[test]
706    fn composed_stack_remove_arrow_error() {
707        let s1 = InMemoryStack::<u8, u8, u8>::new();
708        let s2 = InMemoryStack::<u8, u8, u8>::new();
709        let mut cs = ComposedStack::new(&s1, &s2, |a, _b| *a);
710        let err = cs.remove_arrow(0, 0).unwrap_err();
711        assert_eq!(
712            err.to_string(),
713            MeshSieveError::UnsupportedStackOperation("remove_arrow on ComposedStack").to_string()
714        );
715    }
716    #[test]
717    fn composed_stack_base_mut_error() {
718        let s1 = InMemoryStack::<u8, u8, u8>::new();
719        let s2 = InMemoryStack::<u8, u8, u8>::new();
720        let mut cs = ComposedStack::new(&s1, &s2, |a, _b| *a);
721        let err = cs.base_mut().unwrap_err();
722        assert_eq!(
723            err.to_string(),
724            MeshSieveError::UnsupportedStackOperation("base_mut on ComposedStack").to_string()
725        );
726    }
727    #[test]
728    fn composed_stack_cap_mut_error() {
729        let s1 = InMemoryStack::<u8, u8, u8>::new();
730        let s2 = InMemoryStack::<u8, u8, u8>::new();
731        let mut cs = ComposedStack::new(&s1, &s2, |a, _b| *a);
732        let err = cs.cap_mut().unwrap_err();
733        assert_eq!(
734            err.to_string(),
735            MeshSieveError::UnsupportedStackOperation("cap_mut on ComposedStack").to_string()
736        );
737    }
738
739    #[test]
740    fn remove_nonexistent_returns_none() {
741        use crate::topology::sieve::MutableSieve;
742        let mut s = InMemoryStack::<u32, u32, ()>::new();
743        MutableSieve::add_base_point(s.base_mut().unwrap(), 5);
744        MutableSieve::add_cap_point(s.cap_mut().unwrap(), 50);
745        assert_eq!(s.remove_arrow(5, 50).unwrap(), None);
746    }
747
748    #[test]
749    fn remove_arrow_cleans_empty_maps() {
750        let mut s = InMemoryStack::<u32, u32, ()>::new();
751        s.base.add_arrow(1, 1, ());
752        s.cap.add_arrow(10, 10, ());
753        let _ = s.add_arrow(1, 10, ());
754        // Remove the only arrow and ensure maps no longer report the points
755        assert_eq!(s.remove_arrow(1, 10).unwrap(), Some(()));
756        assert!(s.base_points().next().is_none());
757        assert!(s.cap_points().next().is_none());
758    }
759
760    #[test]
761    fn invalidate_cache_noop() {
762        let mut s = InMemoryStack::<u32, u32, i32>::new();
763        s.base.add_arrow(1, 2, ());
764        s.cap.add_arrow(10, 20, ());
765        let _ = s.base.chart_points();
766        let _ = s.cap.chart_points();
767        assert!(s.base.strata.get().is_some());
768        assert!(s.cap.strata.get().is_some());
769        s.invalidate_cache();
770        assert!(s.base.strata.get().is_some());
771        assert!(s.cap.strata.get().is_some());
772    }
773
774    #[test]
775    fn lift_drop_empty_iter() {
776        let s = InMemoryStack::<u8, u8, ()>::new();
777        assert!(s.lift(0).next().is_none());
778        assert!(s.drop(0).next().is_none());
779    }
780
781    #[test]
782    fn stack_add_arrow_missing_base_rejected() {
783        use crate::topology::sieve::MutableSieve;
784        let mut st = InMemoryStack::<u32, u32, ()>::new();
785        MutableSieve::add_cap_point(st.cap_mut().unwrap(), 20);
786        let res =
787            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| st.add_arrow(10, 20, ())));
788        match res {
789            Ok(Err(e)) => assert!(format!("{e}").contains("base")),
790            Err(_) => (), // panic expected in debug/strict builds
791            Ok(Ok(_)) => panic!("expected failure"),
792        }
793    }
794
795    #[test]
796    fn stack_add_arrow_ok_when_points_exist() {
797        use crate::topology::sieve::MutableSieve;
798        let mut st = InMemoryStack::<u32, u32, ()>::new();
799        MutableSieve::add_base_point(st.base_mut().unwrap(), 10);
800        MutableSieve::add_cap_point(st.cap_mut().unwrap(), 20);
801        st.add_arrow(10, 20, ()).unwrap();
802    }
803
804    #[cfg(any(debug_assertions, feature = "strict-invariants"))]
805    #[test]
806    #[should_panic]
807    fn strict_build_panics_on_missing_cap() {
808        use crate::topology::sieve::MutableSieve;
809        let mut st = InMemoryStack::<u32, u32, ()>::new();
810        MutableSieve::add_base_point(st.base_mut().unwrap(), 10);
811        let _ = st.add_arrow(10, 20, ());
812    }
813
814    // #[test]
815    // #[should_panic]
816    // fn composed_stack_base_panics() {
817    //     let s1 = InMemoryStack::<u8,u8,u8>::new();
818    //     let s2 = InMemoryStack::<u8,u8,u8>::new();
819    //     let cs = ComposedStack::new(&s1, &s2, |a,_b| *a);
820    //     let _ = cs.base();
821    // }
822    // #[test]
823    // #[should_panic]
824    // fn composed_stack_cap_panics() {
825    //     let s1 = InMemoryStack::<u8,u8,u8>::new();
826    //     let s2 = InMemoryStack::<u8,u8,u8>::new();
827    //     let cs = ComposedStack::new(&s1, &s2, |a,_b| *a);
828    //     let _ = cs.cap();
829    // }
830
831    #[test]
832    fn stack_vertical_arrows_are_correct() {
833        let mut s = InMemoryStack::<u32, u32, i32>::new();
834        s.base.add_arrow(2, 2, ());
835        s.cap.add_arrow(20, 20, ());
836        let _ = s.add_arrow(2, 20, 5);
837        // The stack's lift and drop reflect the vertical arrows
838        let lifted: Vec<_> = s.lift(2).collect();
839        assert_eq!(lifted, vec![(20, 5)]);
840        let dropped: Vec<_> = s.drop(20).collect();
841        assert_eq!(dropped, vec![(2, 5)]);
842    }
843}