Skip to content

: selection: routing: handle evals of selections over 0-dim slices #507

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion ndslice/src/selection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Box<dyn Iterator<Item = usize> + '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::<Vec<_>>()
.into_iter(),
));
}

Ok(self
.validate(opts, slice)?
.eval_rec(slice, vec![0; slice.num_dim()], 0))
}

fn eval_rec<'a>(
Expand Down Expand Up @@ -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();
Expand Down
72 changes: 72 additions & 0 deletions ndslice/src/selection/routing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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);
}
}
Loading