From e36b4d1f417b088483fd57c8060fd4406a95d49f Mon Sep 17 00:00:00 2001 From: Shayne Fletcher Date: Sun, 13 Jul 2025 07:34:27 -0700 Subject: [PATCH] : selection: routing: handle evals of selections over 0-dim slices (#507) Summary: handle 0d slices by canonically embedding them as 1d slices of extent 1, enabling uniform evaluation and routing logic. adds test coverage for `eval` and `next_steps`. Differential Revision: D78168758 --- ndslice/src/selection.rs | 44 ++++++++++++++++++- ndslice/src/selection/routing.rs | 72 ++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 1 deletion(-) diff --git a/ndslice/src/selection.rs b/ndslice/src/selection.rs index 3f895ec5..11415e47 100644 --- a/ndslice/src/selection.rs +++ b/ndslice/src/selection.rs @@ -560,12 +560,39 @@ impl Selection { /// feasible because the precise type depends on dynamic selection /// structure. Boxing erases this variability and allows a uniform /// return type. + /// + /// # Canonical handling of 0-dimensional slices + /// + /// A `Slice` with zero dimensions represents the empty product + /// `∏_{i=1}^{0} Xᵢ`, which has exactly one element: the empty + /// tuple. To ensure that evaluation behaves uniformly across + /// dimensions, we canonically embed the 0-dimensional case into a + /// 1-dimensional slice of extent 1. That is, we reinterpret the + /// 0D slice as `Slice::new(offset, [1], [1])`, which is + /// semantically equivalent and enables evaluation to proceed + /// through the normal recursive machinery without special-casing. + /// The result is that selection expressions are always evaluated + /// over a slice with at least one dimension, and uniform logic + /// applies. pub fn eval<'a>( &self, opts: &EvalOpts, slice: &'a Slice, ) -> Result + 'a>, ShapeError> { - Ok(Self::validate(self, opts, slice)?.eval_rec(slice, vec![0; slice.num_dim()], 0)) + // Canonically embed 0D as 1D (extent 1). + if slice.num_dim() == 0 { + let slice = Slice::new(slice.offset(), vec![1], vec![1]).unwrap(); + return Ok(Box::new( + self.validate(opts, &slice)? + .eval_rec(&slice, vec![0; 1], 0) + .collect::>() + .into_iter(), + )); + } + + Ok(self + .validate(opts, slice)? + .eval_rec(slice, vec![0; slice.num_dim()], 0)) } fn eval_rec<'a>( @@ -1831,6 +1858,21 @@ mod tests { assert_matches!(res.as_slice(), [i, j] if *i < *j && *i < 8 && *j < 8); } + #[test] + fn test_eval_zero_dim_slice() { + let slice_0d = Slice::new(1, vec![], vec![]).unwrap(); + // Let s be a slice with dim(s) = 0. Then: ∃! x ∈ s : + // coordsₛ(x) = (). + assert_eq!(slice_0d.coordinates(1).unwrap(), vec![]); + + assert_eq!(eval(true_(), &slice_0d), vec![1]); + assert_eq!(eval(false_(), &slice_0d), vec![]); + assert_eq!(eval(all(true_()), &slice_0d), vec![1]); + assert_eq!(eval(all(false_()), &slice_0d), vec![]); + assert_eq!(eval(union(true_(), true_()), &slice_0d), vec![1]); + assert_eq!(eval(intersection(true_(), false_()), &slice_0d), vec![]); + } + #[test] fn test_selection_10() { let slice = &test_slice(); diff --git a/ndslice/src/selection/routing.rs b/ndslice/src/selection/routing.rs index 7f7db729..6b9440f2 100644 --- a/ndslice/src/selection/routing.rs +++ b/ndslice/src/selection/routing.rs @@ -360,6 +360,26 @@ impl RoutingFrame { /// /// --- /// + /// ### Canonical Handling of Zero-Dimensional Slices + /// + /// A `Slice` with zero dimensions represents the empty product + /// `∏_{i=1}^{0} Xᵢ`, which has exactly one element: the empty + /// tuple. To maintain uniform routing semantics, we canonically + /// embed such 0D slices as 1D slices of extent 1: + /// + /// ```text + /// Slice::new(offset, [1], [1]) + /// ``` + /// + /// This embedding preserves the correct number of addressable + /// points and allows the routing machinery to proceed through the + /// usual recursive strategy without introducing special cases. The + /// selected coordinate is `vec![0]`, and `dim = 0` proceeds as + /// usual. This makes the routing logic consistent with evaluation + /// and avoids edge case handling throughout the codebase. + /// + /// --- + /// /// ### Summary /// /// - **Structure-driven**: Mirrors the shape of the selection @@ -378,6 +398,14 @@ impl RoutingFrame { _chooser: &mut dyn FnMut(&Choice) -> usize, f: &mut dyn FnMut(RoutingStep) -> ControlFlow<()>, ) -> ControlFlow<()> { + if self.slice.num_dim() == 0 { + // Canonically embed 0D as 1D (extent 1). + let embedded = Slice::new(self.slice.offset(), vec![1], vec![1]).unwrap(); + let mut this = self.clone(); + this.slice = Arc::new(embedded); + this.here = vec![0]; + return this.next_steps(_chooser, f); + } let selection = self .selection .clone() @@ -1590,4 +1618,48 @@ mod tests { "Expected panic due to overdelivery, but no panic occurred" ); } + + #[test] + fn test_next_steps_zero_dim_slice() { + use std::ops::ControlFlow; + + use crate::selection::dsl::*; + + let slice = Slice::new(42, vec![], vec![]).unwrap(); + let selection = true_(); + let frame = RoutingFrame::root(selection, slice.clone()); + + let mut steps = vec![]; + let _ = frame.next_steps( + &mut |_| panic!("Unexpected Choice in 0D test"), + &mut |step| { + steps.push(step); + ControlFlow::Continue(()) + }, + ); + + assert_eq!(steps.len(), 1); + let step = steps[0].as_forward().unwrap(); + assert_eq!(step.here, vec![0]); + assert!(step.deliver_here()); + assert_eq!(step.slice.location(&step.here).unwrap(), 42); + + let selection = all(false_()); + let frame = RoutingFrame::root(selection, slice); + + let mut steps = vec![]; + let _ = frame.next_steps( + &mut |_| panic!("Unexpected Choice in 0D test"), + &mut |step| { + steps.push(step); + ControlFlow::Continue(()) + }, + ); + + assert_eq!(steps.len(), 1); + let step = steps[0].as_forward().unwrap(); + assert_eq!(step.here, vec![0]); + assert!(!step.deliver_here()); + assert_eq!(step.slice.location(&step.here).unwrap(), 42); + } }