ndslice/
strategy.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! Property-based generators for [`Selection`] and related types.
10//!
11//! These strategies are used in `proptest`-based tests to construct
12//! randomized selection expressions for testing evaluation, routing,
13//! and normalization logic.
14//!
15//! The main entry point is [`gen_selection(depth)`], which generates
16//! a structurally diverse [`Selection`] of bounded depth, supporting
17//! the `True`, `Range`, `All`, `Union`, and `Intersection`
18//! constructors.
19//!
20//! Example usage:
21//!
22//! ```
23//! use ndslice::strategy::gen_selection;
24//! use proptest::prelude::*;
25//!
26//! proptest! {
27//!     #[test]
28//!     fn test_selection(s in gen_selection(3)) {
29//!         // Use `s` as input to evaluation or routing tests
30//!     }
31//! }
32//! ```
33
34use proptest::prelude::*;
35
36use crate::Slice;
37use crate::selection::EvalOpts;
38use crate::selection::Selection;
39use crate::selection::dsl;
40use crate::shape::Range;
41use crate::view::Extent;
42use crate::view::Region;
43
44/// Generates a random [`Slice`] with up to `max_dims` dimensions,
45/// where each dimension has a size between 1 and `max_len`
46/// (inclusive).
47///
48/// The slice is constructed using standard row-major layout with no
49/// offset, making it suitable for use in evaluation, routing, and
50/// normalization tests.
51///
52/// This generator is used in property-based tests to provide diverse
53/// input shapes for selection and routing logic.
54///
55/// # Parameters
56///
57/// - `max_dims`: Maximum number of dimensions in the generated slice.
58/// - `max_len`: Maximum size per dimension.
59///
60/// # Example
61///
62/// ```
63/// use ndslice::strategy::gen_slice;
64/// use proptest::prelude::*;
65///
66/// proptest! {
67///     #[test]
68///     fn test_slice_generation(slice in gen_slice(4, 8)) {
69///         assert!(!slice.sizes().is_empty());
70///     }
71/// }
72/// ```
73pub fn gen_slice(max_dims: usize, max_len: usize) -> impl Strategy<Value = Slice> {
74    prop::collection::vec(1..=max_len, 1..=max_dims).prop_map(Slice::new_row_major)
75}
76
77/// Generate a random [`Extent`] with `dims` dimensions, where each
78/// size is in `1..=max_len`.
79///
80/// For example, `gen_extent(1..=4, 1..=8)` generates extents like:
81/// - x=3
82/// - x=2, y=4
83/// - x=2, y=4, z=1, w=5
84pub fn gen_extent(
85    dims: std::ops::RangeInclusive<usize>,
86    max_len: usize,
87) -> impl Strategy<Value = Extent> {
88    prop::collection::vec(1..=max_len, dims).prop_map(|sizes| {
89        let labels = (0..sizes.len())
90            .map(|i| format!("d/{}", i))
91            .collect::<Vec<_>>();
92        Extent::new(labels, sizes).unwrap()
93    })
94}
95
96/// Generate a random [`Region`] strategy for property tests.
97///
98/// This builds on [`gen_extent`], producing a region with the same
99/// randomly chosen dimensionality and sizes, but wrapped as a full
100/// [`Region`] (with labels and strides).
101///
102/// - `dims`: inclusive range of allowed dimensionalities (e.g.
103///   `1..=4`)
104/// - `max_len`: maximum size of any dimension
105pub fn gen_region(
106    dims: std::ops::RangeInclusive<usize>,
107    max_len: usize,
108) -> impl proptest::strategy::Strategy<Value = Region> {
109    gen_extent(dims, max_len).prop_map(Into::into)
110}
111
112/// Generate a random [`Region`] strategy with striding for property
113/// tests.
114///
115/// Similar to [`gen_region`], but each dimension may additionally use
116/// a non-unit step. This produces regions whose underlying slice has
117/// non-contiguous strides, useful for testing strided layouts.
118///
119/// - `dims`: inclusive range of allowed dimensionalities (e.g.
120///   `1..=4`)
121/// - `max_len`: maximum size of any dimension
122/// - `max_step`: maximum stride step size applied to each dimension
123/// - `_max_offset`: reserved for future use (currently ignored)
124pub fn gen_region_strided(
125    dims: std::ops::RangeInclusive<usize>,
126    max_len: usize,
127    max_step: usize,
128    _max_offset: usize,
129) -> impl Strategy<Value = Region> {
130    use crate::view::ViewExt;
131
132    prop::collection::vec(1..=max_len, dims)
133        .prop_flat_map(move |sizes| {
134            let n = sizes.len();
135            let labels: Vec<String> = (0..n).map(|i| format!("d/{}", i)).collect();
136
137            let steps_raw = prop::collection::vec(1..=max_step.max(1), n);
138            let begins_unclamped = prop::collection::vec(proptest::num::usize::ANY, n);
139
140            (Just((labels, sizes)), steps_raw, begins_unclamped)
141        })
142        .prop_map(move |((labels, sizes), steps_raw, begins_unclamped)| {
143            // 1) Make steps obey the divisibility chain: step[i] %
144            // step[i+1] == 0
145            let mut steps = steps_raw;
146            if !steps.is_empty() {
147                // innermost is free
148                let last = steps.len() - 1;
149                steps[last] = steps[last].max(1).min(max_step.max(1));
150                // Each outer step is an integer multiple of the next
151                // inner step.
152                for i in (0..last).rev() {
153                    let inner = steps[i + 1].max(1);
154                    let max_mult = (max_step / inner).max(1);
155                    // Clamp current to be a multiple of `inner`
156                    // within [inner, max_step].
157                    let m = ((steps[i].max(1) - 1) % max_mult) + 1;
158                    steps[i] = inner.saturating_mul(m);
159                }
160            }
161
162            // 2) Build from a row-major region and compose per-axis
163            // ranges
164            let mut region: Region = Extent::new(labels.clone(), sizes.clone()).unwrap().into();
165            for i in 0..sizes.len() {
166                let begin = begins_unclamped[i] % sizes[i]; // in [0, size-1]
167                let step = steps[i].max(1);
168                region = region
169                    .range(&labels[i], Range(begin, None, step))
170                    .expect("range stayed rectangular");
171            }
172            region
173        })
174}
175
176/// Generates a pair `(base, subview)` where:
177/// - `base` is a randomly shaped row-major `Slice`,
178/// - `subview` is a valid rectangular region within `base`.
179pub fn gen_slice_and_subview(
180    max_dims: usize,
181    max_len: usize,
182) -> impl Strategy<Value = (Slice, Slice)> {
183    assert!(max_dims <= 8, "Supports up to 4 dimensions explicitly");
184
185    gen_slice(max_dims, max_len).prop_flat_map(|base| {
186        let sizes = base.sizes().to_vec();
187
188        // Strategy per dimension
189        let dim_strat = |extent| {
190            if extent == 0 {
191                Just((0, 0)).boxed()
192            } else {
193                (0..extent)
194                    .prop_flat_map(move |start| {
195                        (1..=extent - start).prop_map(move |len| (start, len))
196                    })
197                    .boxed()
198            }
199        };
200
201        // Explicit match based on dims
202        match sizes.len() {
203            1 => (dim_strat(sizes[0]),).prop_map(|(a,)| vec![a]).boxed(),
204            2 => (dim_strat(sizes[0]), dim_strat(sizes[1]))
205                .prop_map(|(a, b)| vec![a, b])
206                .boxed(),
207            3 => (
208                dim_strat(sizes[0]),
209                dim_strat(sizes[1]),
210                dim_strat(sizes[2]),
211            )
212                .prop_map(|(a, b, c)| vec![a, b, c])
213                .boxed(),
214            4 => (
215                dim_strat(sizes[0]),
216                dim_strat(sizes[1]),
217                dim_strat(sizes[2]),
218                dim_strat(sizes[3]),
219            )
220                .prop_map(|(a, b, c, d)| vec![a, b, c, d])
221                .boxed(),
222            5 => (
223                dim_strat(sizes[0]),
224                dim_strat(sizes[1]),
225                dim_strat(sizes[2]),
226                dim_strat(sizes[3]),
227                dim_strat(sizes[4]),
228            )
229                .prop_map(|(a, b, c, d, e)| vec![a, b, c, d, e])
230                .boxed(),
231            6 => (
232                dim_strat(sizes[0]),
233                dim_strat(sizes[1]),
234                dim_strat(sizes[2]),
235                dim_strat(sizes[3]),
236                dim_strat(sizes[4]),
237                dim_strat(sizes[5]),
238            )
239                .prop_map(|(a, b, c, d, e, f)| vec![a, b, c, d, e, f])
240                .boxed(),
241            7 => (
242                dim_strat(sizes[0]),
243                dim_strat(sizes[1]),
244                dim_strat(sizes[2]),
245                dim_strat(sizes[3]),
246                dim_strat(sizes[4]),
247                dim_strat(sizes[5]),
248                dim_strat(sizes[6]),
249            )
250                .prop_map(|(a, b, c, d, e, f, g)| vec![a, b, c, d, e, f, g])
251                .boxed(),
252            8 => (
253                dim_strat(sizes[0]),
254                dim_strat(sizes[1]),
255                dim_strat(sizes[2]),
256                dim_strat(sizes[3]),
257                dim_strat(sizes[4]),
258                dim_strat(sizes[5]),
259                dim_strat(sizes[6]),
260                dim_strat(sizes[7]),
261            )
262                .prop_map(|(a, b, c, d, e, f, g, h)| vec![a, b, c, d, e, f, g, h])
263                .boxed(),
264            _ => unreachable!("max_dims constrained to 8"),
265        }
266        .prop_map(move |ranges| {
267            let (starts, lens): (Vec<_>, Vec<_>) = ranges.into_iter().unzip();
268            let subview = base.subview(&starts, &lens).expect("valid subview");
269            (base.clone(), subview)
270        })
271    })
272}
273
274/// Recursively generates a random `Selection` expression of bounded
275/// depth, aligned with the given slice `shape`.
276///
277/// Each recursive call corresponds to one dimension of the shape,
278/// starting from `dim`, and constructs a selection operator (`range`,
279/// `all`, `intersection`, etc.) that applies at that level.
280///
281/// The recursion proceeds until either:
282/// - `depth == 0`, which limits structural complexity, or
283/// - `dim >= shape.len()`, which prevents exceeding the
284///   dimensionality.
285///
286/// In both cases, the recursion terminates with a `true_()` leaf
287/// node, effectively selecting all remaining elements.
288///
289/// The resulting selections are guaranteed to be valid under a strict
290/// validation regime: they contain no empty ranges, no out-of-bounds
291/// accesses, and no dynamic constructs like `any` or `first`.
292pub fn gen_selection(depth: u32, shape: Vec<usize>, dim: usize) -> BoxedStrategy<Selection> {
293    let leaf = Just(dsl::true_()).boxed();
294
295    if depth == 0 || dim >= shape.len() {
296        return leaf;
297    }
298
299    let recur = {
300        let shape = shape.clone();
301        move || gen_selection(depth - 1, shape.clone(), dim + 1)
302    };
303
304    let range_strategy = {
305        let dim_size = shape[dim];
306        (0..dim_size)
307            .prop_flat_map(move |start| {
308                let max_len = dim_size - start;
309                (1..=max_len).prop_flat_map(move |len| {
310                    (1..=len).prop_map(move |step| {
311                        let r = Range(start, Some(start + len), step);
312                        dsl::range(r, dsl::true_())
313                    })
314                })
315            })
316            .boxed()
317    };
318
319    let all = recur().prop_map(dsl::all).boxed();
320
321    let union = (recur(), recur())
322        .prop_map(|(a, b)| dsl::union(a, b))
323        .boxed();
324
325    let inter = (recur(), recur())
326        .prop_map(|(a, b)| dsl::intersection(a, b))
327        .boxed();
328
329    prop_oneof![
330        2 => leaf,
331        3 => range_strategy,
332        3 => all,
333        2 => union,
334        2 => inter,
335    ]
336    .prop_filter("valid selection", move |s| {
337        let slice = Slice::new_row_major(shape.clone());
338        let eval_opts = EvalOpts {
339            disallow_dynamic_selections: true,
340            ..EvalOpts::strict()
341        };
342        s.validate(&eval_opts, &slice).is_ok()
343    })
344    .boxed()
345}
346
347#[cfg(test)]
348mod tests {
349    use std::collections::HashMap;
350    use std::collections::HashSet;
351
352    use proptest::strategy::ValueTree;
353    use proptest::test_runner::Config;
354    use proptest::test_runner::TestRunner;
355
356    use super::*;
357    use crate::selection::EvalOpts;
358    use crate::selection::routing::RoutingFrame;
359    use crate::selection::test_utils::collect_commactor_routing_tree;
360    use crate::selection::test_utils::collect_routed_paths;
361
362    #[test]
363    fn print_some_slices() {
364        let mut runner = TestRunner::new(Config::default());
365
366        for _ in 0..256 {
367            let strat = gen_slice(4, 8); // up to 4 dimensions, max size per dim = 8
368            let value = strat.new_tree(&mut runner).unwrap().current();
369            println!("{:?}", value);
370        }
371    }
372
373    proptest! {
374        #[test]
375        fn test_slice_properties(slice in gen_slice(4, 8)) {
376            let total_size: usize = slice.sizes().iter().product();
377            prop_assert!(total_size > 0);
378        }
379    }
380
381    #[test]
382    fn print_some_selections() {
383        let mut runner = TestRunner::new(Config::default());
384
385        for _ in 0..256 {
386            let strat = gen_selection(3, vec![2, 4, 8], 0);
387            let value = strat.new_tree(&mut runner).unwrap().current();
388            println!("{:?}", value);
389        }
390    }
391
392    // Test `trace_route` exhibits path determinism.
393    //
394    // This test instantiates a general theorem about the selection
395    // algebra and its routing semantics:
396    //
397    //   ∀ `S`, `T`, and `n`,
398    //     `n ∈ eval(S, slice)` ∧ `n ∈ eval(T, slice)` ⇒
399    //     `route(n, S) == route(n, T)`.
400    //
401    // This property enables us to enforce in-order delivery using
402    // only per-sender, per-peer sequence numbers. Since every message
403    // to a given destination follows the same deterministic path
404    // through the mesh, intermediate nodes can forward messages in
405    // order, and receivers can detect missing or out-of-order
406    // messages using only local state.
407    //
408    // This test uses `trace_route` to observe the path to each
409    // overlapping destination node under `S` and `T`, asserting that
410    // the results agree.
411    proptest! {
412        #![proptest_config(ProptestConfig {
413            cases: 8, ..ProptestConfig::default()
414        })]
415        #[test]
416        fn trace_route_path_determinism(
417            slice in gen_slice(4, 8)
418        ) {
419            let shape = slice.sizes().to_vec();
420
421            let mut runner = TestRunner::default();
422            let s = gen_selection(4, shape.clone(), 0).new_tree(&mut runner).unwrap().current();
423            let t = gen_selection(4, shape.clone(), 0).new_tree(&mut runner).unwrap().current();
424
425            let eval_opts = EvalOpts::strict();
426            let sel_s: HashSet<_> = s.eval(&eval_opts, &slice).unwrap().collect();
427            let sel_t: HashSet<_> = t.eval(&eval_opts, &slice).unwrap().collect();
428            let ranks: Vec<_> = sel_s.intersection(&sel_t).cloned().collect();
429
430            if ranks.is_empty() {
431                println!("skipping empty intersection");
432            } else {
433                println!("testing {} nodes", ranks.len());
434                for rank in ranks {
435                    let coords = slice.coordinates(rank).unwrap();
436                    let start_s = RoutingFrame::root(s.clone(), slice.clone());
437                    let start_t = RoutingFrame::root(t.clone(), slice.clone());
438                    let path_s = start_s.trace_route(&coords).unwrap();
439                    let path_t = start_t.trace_route(&coords).unwrap();
440                    prop_assert_eq!(
441                        path_s.clone(),
442                        path_t.clone(),
443                        "path to {:?} differs under S and T\nS path: {:?}\nT path: {:?}",
444                        rank, path_s, path_t
445                    );
446                }
447            }
448        }
449    }
450
451    // Test `collect_routed_paths` exhibits path determinism.
452    //
453    // This test instantiates the same fundamental property as the
454    // `trace_route` test, but does so using `collect_routed_paths`,
455    // which performs a breadth-first traversal of the routing tree.
456    //
457    //   ∀ `S`, `T`, and `n`,
458    //     `n ∈ eval(S, slice)` ∧ `n ∈ eval(T, slice)` ⇒
459    //     `route(n, S) == route(n, T)`.
460    //
461    // The property guarantees that every destination node reachable
462    // by both `S` and `T` is routed to via the same deterministic
463    // path.
464    //
465    // This test avoids calls to `eval` by intersecting the routed
466    // destinations directly. For each rank routed to by both
467    // selections, it compares the path returned by
468    // `collect_routed_paths`, ensuring the selection algebra routes
469    // consistently regardless of expression structure or traversal
470    // order.
471    proptest! {
472        #![proptest_config(ProptestConfig {
473            cases: 128, ..ProptestConfig::default()
474        })]
475        #[test]
476        fn collect_routed_path_determinism(
477            slice in gen_slice(4, 8)
478        ) {
479            let shape = slice.sizes().to_vec();
480
481            let mut runner = TestRunner::default();
482            let s = gen_selection(4, shape.clone(), 0).new_tree(&mut runner).unwrap().current();
483            let t = gen_selection(4, shape.clone(), 0).new_tree(&mut runner).unwrap().current();
484
485            let paths_s = collect_routed_paths(&s, &slice);
486            let paths_t = collect_routed_paths(&t, &slice);
487            let ranks: Vec<_> = paths_s.delivered.keys()
488                .filter(|r| paths_t.delivered.contains_key(*r))
489                .cloned()
490                .collect();
491
492            if ranks.is_empty() {
493                println!("skipping empty intersection");
494            } else {
495                println!("testing {} nodes", ranks.len());
496                for rank in ranks {
497                    let path_s = paths_s.delivered.get(&rank).unwrap();
498                    let path_t = paths_t.delivered.get(&rank).unwrap();
499                    prop_assert_eq!(
500                        path_s.clone(),
501                        path_t.clone(),
502                        "path to {:?} differs under S and T\nS path: {:?}\nT path: {:?}",
503                        rank, path_s, path_t
504                    );
505                }
506            }
507        }
508    }
509
510    // Test `collect_commactor_routing_tree` exhibits path
511    // determinism.
512    //
513    // This test instantiates the same routing path determinism
514    // property as in `collect_routed_path_determinism`, but uses the
515    // full CommActor-style routing simulation instead.
516    //
517    //   ∀ `S`, `T`, and `n`,
518    //     `n ∈ eval(S, slice)` ∧ `n ∈ eval(T, slice)` ⇒
519    //     `route(n, S) == route(n, T)`.
520    //
521    // This ensures that every destination rank reachable by both `S`
522    // and `T` receives its message along the same logical path, even
523    // when selection expressions differ structurally.
524    //
525    // The test avoids explicit calls to eval by intersecting the
526    // delivered ranks from both traversals. For each rank delivered
527    // to by both selections, it compares the delivery path recorded
528    // in `CommActorRoutingTree::delivered`. This validates that
529    // CommActor message routing is structurally deterministic.
530    proptest! {
531        #![proptest_config(ProptestConfig {
532            cases: 128, ..ProptestConfig::default()
533        })]
534        #[test]
535        fn collect_commactor_routed_path_determinism(
536            slice in gen_slice(4, 8)
537        ) {
538            let extents = slice.sizes().to_vec();
539
540            let mut runner = TestRunner::default();
541            let s = gen_selection(4, extents.clone(), 0).new_tree(&mut runner).unwrap().current();
542            let t = gen_selection(4, extents.clone(), 0).new_tree(&mut runner).unwrap().current();
543
544            let tree_s = collect_commactor_routing_tree(&s, &slice);
545            let tree_t = collect_commactor_routing_tree(&t, &slice);
546
547            let ranks: Vec<_> = tree_s
548                .delivered
549                .keys()
550                .filter(|r| tree_t.delivered.contains_key(*r))
551                .cloned()
552                .collect();
553
554            if ranks.is_empty() {
555                println!("skipping empty intersection");
556            } else {
557                println!("testing {} nodes", ranks.len());
558                for rank in ranks {
559                    let path_s = &tree_s.delivered[&rank];
560                    let path_t = &tree_t.delivered[&rank];
561                    prop_assert_eq!(
562                        path_s.clone(),
563                        path_t.clone(),
564                        "path to {:?} differs under S and T\nS path: {:?}\nT path: {:?}",
565                        rank, path_s, path_t
566                    );
567                }
568            }
569        }
570    }
571
572    // Property test: Unique Predecessor Theorem
573    //
574    // This test verifies a structural invariant of the routing graph
575    // produced by `collect_routed_paths`, which performs a
576    // breadth-first traversal of a selection over a multidimensional
577    // mesh.
578    //
579    // ───────────────────────────────────────────────────────────────
580    // Unique Predecessor Theorem
581    //
582    // In a full routing traversal, each coordinate `x` is the target
583    // of at most one `RoutingStep::Forward` from a distinct
584    // coordinate `y ≠ x`.
585    //
586    // Any additional frames that reach `x` arise only from:
587    //   - self-forwarding (i.e., `x → x`)
588    //   - structural duplication from the same parent node (e.g., via
589    //     unions)
590    //
591    // This ensures that routing paths form a tree-like structure
592    // rooted at the origin, with no multiple distinct predecessors
593    // except in the degenerate (self-loop) or duplicated-parent
594    // cases.
595    proptest! {
596        #![proptest_config(ProptestConfig {
597            cases: 256, ..ProptestConfig::default()
598        })]
599        #[test]
600        fn collect_routed_paths_unique_predecessor(
601            slice in gen_slice(4, 8)
602        ) {
603            let shape = slice.sizes().to_vec();
604
605            let mut runner = TestRunner::default();
606            let s = gen_selection(4, shape.clone(), 0).new_tree(&mut runner).unwrap().current();
607
608            let tree = collect_routed_paths(&s, &slice);
609
610            for (node, preds) in tree.predecessors {
611                let non_self_preds: Vec<_> = preds.clone().into_iter()
612                    .filter(|&p| p != node)
613                    .collect();
614
615                prop_assert!(
616                    non_self_preds.len() <= 1,
617                    "Node {} had multiple non-self predecessors: {:?} (selection: {})",
618                    node,
619                    non_self_preds,
620                    s,
621                );
622            }
623        }
624    }
625
626    // Property test: Unique Predecessor Theorem (CommActor Routing)
627    //
628    // This test verifies structural invariants of the routing graph
629    // produced by `collect_commactor_routing_tree`, which simulates
630    // CommActor-style peer-to-peer multicast forwarding.
631    //
632    // ───────────────────────────────────────────────────────────────
633    // Unique Predecessor Theorem
634    //
635    // In a full routing traversal, each coordinate `x` is the target
636    // of at most one `RoutingStep::Forward` from a distinct
637    // coordinate `y ≠ x`.
638    //
639    // Any additional frames that reach `x` arise only from:
640    //   - structural duplication from the same parent node (e.g., via
641    //     unions)
642    //
643    // Unlike the general `collect_routed_paths`, CommActor routing
644    // never performs self-forwarding (`x → x`). This test confirms
645    // that as well.
646    proptest! {
647        #![proptest_config(ProptestConfig {
648            cases: 256, ..ProptestConfig::default()
649        })]
650        #[test]
651        fn commactor_routed_paths_unique_predecessor(
652            slice in gen_slice(4, 8)
653        ) {
654            let shape = slice.sizes().to_vec();
655
656            let mut runner = TestRunner::default();
657            let s = gen_selection(4, shape.clone(), 0).new_tree(&mut runner).unwrap().current();
658
659            let tree = collect_commactor_routing_tree(&s, &slice);
660
661            let mut preds: HashMap<usize, HashSet<usize>> = HashMap::new();
662
663            for (from, frames) in &tree.forwards {
664                for frame in frames {
665                    let to = slice.location(&frame.here).unwrap();
666
667                    // We assert that a CommActor never forwards to
668                    // itself.
669                    prop_assert_ne!(
670                        *from, to,
671                        "CommActor forwarded to itself: {} → {} (selection: {})",
672                        from, to, s
673                    );
674
675                    preds.entry(to).or_default().insert(*from);
676                }
677            }
678
679            for (node, parents) in preds {
680                let non_self_preds: Vec<_> = parents.into_iter()
681                    .filter(|&p| p != node)
682                    .collect();
683
684                prop_assert!(
685                    non_self_preds.len() <= 1,
686                    "Node {} had multiple non-self predecessors: {:?} (selection: {})",
687                    node,
688                    non_self_preds,
689                    s,
690                );
691            }
692        }
693    }
694
695    // Theorem (Subview-Coordinate Inclusion):
696    //
697    // For any rectangular subview `V` of a base slice `B`, each
698    // coordinate `v` in `V` maps to a coordinate `b = view_offset +
699    // v` that must be in `B`.
700    //
701    // This test verifies that all coordinates of a generated subview
702    // are valid when translated back into the coordinate system of
703    // the base slice.
704    proptest! {
705        #[test]
706        fn test_gen_slice_and_subview((base, subview) in gen_slice_and_subview(4, 8)) {
707            for idx in subview.iter() {
708                let v = subview.coordinates(idx).unwrap();
709                let view_offset_in_base = base.coordinates(subview.offset()).unwrap();
710
711                // b = view_offset + v
712                let b: Vec<_> = v.iter()
713                    .zip(&view_offset_in_base)
714                    .map(|(sub_c, offset)| sub_c + offset)
715                    .collect();
716
717                assert!(base.location(&b).is_ok());
718            }
719        }
720    }
721
722    // Coordinate–Rank Isomorphism for Extents
723
724    // Theorem 1: Rank is injective on valid points
725    //
726    // For a given Extent, every distinct coordinate (i.e. Point)
727    // maps to a unique rank.
728    //
729    // ∀ p ≠ q ∈ extent.iter(),  p.rank() ≠ q.rank()
730    proptest! {
731      #[test]
732      fn rank_is_injective(extent in gen_extent(1..=4, 8)) {
733        let mut seen = HashSet::new();
734        for point in extent.points() {
735          let rank = point.rank();
736          prop_assert!(
737            seen.insert(rank),
738            "Duplicate rank {} for point {}",
739            rank,
740            point
741          );
742        }
743      }
744    }
745
746    // Theorem 2: Row-major monotonicity
747    //
748    // The rank function is monotonic in lexicographic (row-major)
749    // coordinate order.
750    //
751    // ∀ p, q ∈ ℕᵈ, p ≺ q ⇒ rank(p) < rank(q)
752    proptest! {
753      #[test]
754      fn rank_is_monotonic(extent in gen_extent(1..=4, 8)) {
755        let mut last_rank = None;
756        for point in extent.points() {
757          let rank = point.rank();
758          if let Some(prev) = last_rank {
759            prop_assert!(prev < rank, "Rank not monotonic: {} >= {}", prev, rank);
760          }
761          last_rank = Some(rank);
762        }
763      }
764    }
765
766    // Theorem 3: Rank bounds
767    //
768    // For any point p in extent E, the rank of p is in the range:
769    //     0 ≤ rank(p) < E.len()
770    //
771    // ∀ p ∈ E, 0 ≤ rank(p) < |E|
772    proptest! {
773      #[test]
774      fn rank_bounds(extent in gen_extent(1..=4, 8)) {
775        let len = extent.num_ranks();
776        for point in extent.points() {
777          let rank = point.rank();
778          prop_assert!(rank < len, "Rank {} out of bounds for extent of size {}", rank, len);
779        }
780      }
781    }
782
783    // Theorem 4: Isomorphism (Rank-point round-trip is identity on
784    // all ranks)
785    //
786    // For every valid rank r ∈ [0, extent.len()), converting it to a
787    // point and back gives the same rank:
788    //
789    //     rank(point_of_rank(r)) = r
790    //
791    // In categorical terms: rank ∘ point_of_rank = 𝟙
792    proptest! {
793        #[test]
794        fn rank_point_trip(extent in gen_extent(1..=4, 8)) {
795            for r in 0..extent.num_ranks() {
796                let point = extent.point_of_rank(r).unwrap();
797                prop_assert_eq!(
798                    point.rank(),
799                    r,
800                    "point_of_rank({}) returned {}, which maps to rank {}",
801                    r,
802                    point,
803                    point.rank()
804                );
805            }
806        }
807    }
808
809    // Theorem 5: Isomorphism (Point–Coords–Rank round-trip is
810    // identity on all points)
811    //
812    // For every point p ∈ extent.points(), converting its coordinates
813    // back to a rank yields the same rank:
814    //
815    //     rank_of_coords(coords(p)) = rank(p)
816    //
817    // In categorical terms: rank_of_coords ∘ coords = rank
818    proptest! {
819        #[test]
820        fn coords_to_rank_roundtrip(extent in gen_extent(0..=4, 8)) {
821            for p in extent.points() {
822                let c = p.coords();
823                let r = extent.rank_of_coords(&c).expect("coords from Point must be valid");
824                prop_assert_eq!(r, p.rank(), "coords->rank mismatch for {}", p);
825            }
826        }
827    }
828}