Skip to content

Commit e36b4d1

Browse files
: 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
1 parent 442a810 commit e36b4d1

File tree

2 files changed

+115
-1
lines changed

2 files changed

+115
-1
lines changed

ndslice/src/selection.rs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -560,12 +560,39 @@ impl Selection {
560560
/// feasible because the precise type depends on dynamic selection
561561
/// structure. Boxing erases this variability and allows a uniform
562562
/// return type.
563+
///
564+
/// # Canonical handling of 0-dimensional slices
565+
///
566+
/// A `Slice` with zero dimensions represents the empty product
567+
/// `∏_{i=1}^{0} Xᵢ`, which has exactly one element: the empty
568+
/// tuple. To ensure that evaluation behaves uniformly across
569+
/// dimensions, we canonically embed the 0-dimensional case into a
570+
/// 1-dimensional slice of extent 1. That is, we reinterpret the
571+
/// 0D slice as `Slice::new(offset, [1], [1])`, which is
572+
/// semantically equivalent and enables evaluation to proceed
573+
/// through the normal recursive machinery without special-casing.
574+
/// The result is that selection expressions are always evaluated
575+
/// over a slice with at least one dimension, and uniform logic
576+
/// applies.
563577
pub fn eval<'a>(
564578
&self,
565579
opts: &EvalOpts,
566580
slice: &'a Slice,
567581
) -> Result<Box<dyn Iterator<Item = usize> + 'a>, ShapeError> {
568-
Ok(Self::validate(self, opts, slice)?.eval_rec(slice, vec![0; slice.num_dim()], 0))
582+
// Canonically embed 0D as 1D (extent 1).
583+
if slice.num_dim() == 0 {
584+
let slice = Slice::new(slice.offset(), vec![1], vec![1]).unwrap();
585+
return Ok(Box::new(
586+
self.validate(opts, &slice)?
587+
.eval_rec(&slice, vec![0; 1], 0)
588+
.collect::<Vec<_>>()
589+
.into_iter(),
590+
));
591+
}
592+
593+
Ok(self
594+
.validate(opts, slice)?
595+
.eval_rec(slice, vec![0; slice.num_dim()], 0))
569596
}
570597

571598
fn eval_rec<'a>(
@@ -1831,6 +1858,21 @@ mod tests {
18311858
assert_matches!(res.as_slice(), [i, j] if *i < *j && *i < 8 && *j < 8);
18321859
}
18331860

1861+
#[test]
1862+
fn test_eval_zero_dim_slice() {
1863+
let slice_0d = Slice::new(1, vec![], vec![]).unwrap();
1864+
// Let s be a slice with dim(s) = 0. Then: ∃! x ∈ s :
1865+
// coordsₛ(x) = ().
1866+
assert_eq!(slice_0d.coordinates(1).unwrap(), vec![]);
1867+
1868+
assert_eq!(eval(true_(), &slice_0d), vec![1]);
1869+
assert_eq!(eval(false_(), &slice_0d), vec![]);
1870+
assert_eq!(eval(all(true_()), &slice_0d), vec![1]);
1871+
assert_eq!(eval(all(false_()), &slice_0d), vec![]);
1872+
assert_eq!(eval(union(true_(), true_()), &slice_0d), vec![1]);
1873+
assert_eq!(eval(intersection(true_(), false_()), &slice_0d), vec![]);
1874+
}
1875+
18341876
#[test]
18351877
fn test_selection_10() {
18361878
let slice = &test_slice();

ndslice/src/selection/routing.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,26 @@ impl RoutingFrame {
360360
///
361361
/// ---
362362
///
363+
/// ### Canonical Handling of Zero-Dimensional Slices
364+
///
365+
/// A `Slice` with zero dimensions represents the empty product
366+
/// `∏_{i=1}^{0} Xᵢ`, which has exactly one element: the empty
367+
/// tuple. To maintain uniform routing semantics, we canonically
368+
/// embed such 0D slices as 1D slices of extent 1:
369+
///
370+
/// ```text
371+
/// Slice::new(offset, [1], [1])
372+
/// ```
373+
///
374+
/// This embedding preserves the correct number of addressable
375+
/// points and allows the routing machinery to proceed through the
376+
/// usual recursive strategy without introducing special cases. The
377+
/// selected coordinate is `vec![0]`, and `dim = 0` proceeds as
378+
/// usual. This makes the routing logic consistent with evaluation
379+
/// and avoids edge case handling throughout the codebase.
380+
///
381+
/// ---
382+
///
363383
/// ### Summary
364384
///
365385
/// - **Structure-driven**: Mirrors the shape of the selection
@@ -378,6 +398,14 @@ impl RoutingFrame {
378398
_chooser: &mut dyn FnMut(&Choice) -> usize,
379399
f: &mut dyn FnMut(RoutingStep) -> ControlFlow<()>,
380400
) -> ControlFlow<()> {
401+
if self.slice.num_dim() == 0 {
402+
// Canonically embed 0D as 1D (extent 1).
403+
let embedded = Slice::new(self.slice.offset(), vec![1], vec![1]).unwrap();
404+
let mut this = self.clone();
405+
this.slice = Arc::new(embedded);
406+
this.here = vec![0];
407+
return this.next_steps(_chooser, f);
408+
}
381409
let selection = self
382410
.selection
383411
.clone()
@@ -1590,4 +1618,48 @@ mod tests {
15901618
"Expected panic due to overdelivery, but no panic occurred"
15911619
);
15921620
}
1621+
1622+
#[test]
1623+
fn test_next_steps_zero_dim_slice() {
1624+
use std::ops::ControlFlow;
1625+
1626+
use crate::selection::dsl::*;
1627+
1628+
let slice = Slice::new(42, vec![], vec![]).unwrap();
1629+
let selection = true_();
1630+
let frame = RoutingFrame::root(selection, slice.clone());
1631+
1632+
let mut steps = vec![];
1633+
let _ = frame.next_steps(
1634+
&mut |_| panic!("Unexpected Choice in 0D test"),
1635+
&mut |step| {
1636+
steps.push(step);
1637+
ControlFlow::Continue(())
1638+
},
1639+
);
1640+
1641+
assert_eq!(steps.len(), 1);
1642+
let step = steps[0].as_forward().unwrap();
1643+
assert_eq!(step.here, vec![0]);
1644+
assert!(step.deliver_here());
1645+
assert_eq!(step.slice.location(&step.here).unwrap(), 42);
1646+
1647+
let selection = all(false_());
1648+
let frame = RoutingFrame::root(selection, slice);
1649+
1650+
let mut steps = vec![];
1651+
let _ = frame.next_steps(
1652+
&mut |_| panic!("Unexpected Choice in 0D test"),
1653+
&mut |step| {
1654+
steps.push(step);
1655+
ControlFlow::Continue(())
1656+
},
1657+
);
1658+
1659+
assert_eq!(steps.len(), 1);
1660+
let step = steps[0].as_forward().unwrap();
1661+
assert_eq!(step.here, vec![0]);
1662+
assert!(!step.deliver_here());
1663+
assert_eq!(step.slice.location(&step.here).unwrap(), 42);
1664+
}
15931665
}

0 commit comments

Comments
 (0)