Skip to main content

runmat_runtime/indexing/
plan.rs

1use crate::indexing::selectors::{
2    index_scalar_from_value, logical_indices_linear, materialize_index_value,
3    numeric_tensor_indices, SliceSelector,
4};
5use crate::indexing::EndExpr;
6use crate::runtime_error::semantic_error as mex;
7use crate::{builtins::common::shape::is_scalar_shape, RuntimeError};
8use runmat_value::Value;
9use std::future::Future;
10
11pub type VmResult<T> = Result<T, RuntimeError>;
12
13#[derive(Debug, Clone, Default)]
14pub struct IndexPlanProperties {
15    pub full_row: Option<usize>,
16    pub full_column: Option<usize>,
17}
18
19#[derive(Debug, Clone)]
20pub struct IndexPlan {
21    pub indices: Vec<u32>,
22    pub output_shape: Vec<usize>,
23    pub selection_lengths: Vec<usize>,
24    pub dims: usize,
25    pub base_shape: Vec<usize>,
26    pub properties: IndexPlanProperties,
27}
28
29impl IndexPlan {
30    pub fn new(
31        indices: Vec<u32>,
32        output_shape: Vec<usize>,
33        selection_lengths: Vec<usize>,
34        dims: usize,
35        base_shape: Vec<usize>,
36    ) -> Self {
37        let properties = derive_plan_properties(&indices, dims, &base_shape);
38        Self {
39            indices,
40            output_shape,
41            selection_lengths,
42            dims,
43            base_shape,
44            properties,
45        }
46    }
47}
48
49fn derive_plan_properties(
50    indices: &[u32],
51    dims: usize,
52    base_shape: &[usize],
53) -> IndexPlanProperties {
54    let mut properties = IndexPlanProperties {
55        full_row: None,
56        full_column: None,
57    };
58    if dims != 2 || indices.is_empty() {
59        return properties;
60    }
61    let rows = base_shape.first().copied().unwrap_or(1);
62    let cols = base_shape.get(1).copied().unwrap_or(1);
63    if indices.len() == rows {
64        let first = indices[0] as usize;
65        if first.is_multiple_of(rows) {
66            let col = first / rows;
67            if col < cols
68                && indices
69                    .iter()
70                    .enumerate()
71                    .all(|(r, &idx)| idx as usize == col * rows + r)
72            {
73                properties.full_column = Some(col);
74            }
75        }
76    }
77    if indices.len() == cols {
78        let first = indices[0] as usize;
79        let row = first % rows;
80        if row < rows
81            && indices
82                .iter()
83                .enumerate()
84                .all(|(c, &idx)| idx as usize == row + c * rows)
85        {
86            properties.full_row = Some(row);
87        }
88    }
89    properties
90}
91
92fn cartesian_product<F: FnMut(&[usize])>(lists: &[Vec<usize>], mut f: F) {
93    let dims = lists.len();
94    if dims == 0 {
95        return;
96    }
97    let mut idx = vec![0usize; dims];
98    loop {
99        let current: Vec<usize> = (0..dims).map(|d| lists[d][idx[d]]).collect();
100        f(&current);
101        let mut d = 0usize;
102        while d < dims {
103            idx[d] += 1;
104            if idx[d] < lists[d].len() {
105                break;
106            }
107            idx[d] = 0;
108            d += 1;
109        }
110        if d == dims {
111            break;
112        }
113    }
114}
115
116pub fn total_len_from_shape(shape: &[usize]) -> usize {
117    if is_scalar_shape(shape) {
118        1
119    } else {
120        shape.iter().copied().product()
121    }
122}
123
124fn checked_total_len_from_shape(shape: &[usize]) -> VmResult<usize> {
125    if is_scalar_shape(shape) {
126        return Ok(1);
127    }
128    shape.iter().try_fold(1usize, |acc, dim| {
129        acc.checked_mul(*dim)
130            .ok_or_else(|| mex("IndexOutOfBounds", "Index dimensions overflow"))
131    })
132}
133
134fn checked_u32_index(index: usize) -> VmResult<u32> {
135    u32::try_from(index).map_err(|_| mex("IndexOutOfBounds", "Index exceeds supported range"))
136}
137
138fn matlab_squeezed_shape(selection_lengths: &[usize], scalar_mask: &[bool]) -> Vec<usize> {
139    let mut dims: Vec<(usize, usize, bool)> = selection_lengths
140        .iter()
141        .enumerate()
142        .map(|(d, &len)| (d, len, scalar_mask.get(d).copied().unwrap_or(false)))
143        .collect();
144    while dims.len() > 2
145        && dims
146            .last()
147            .map(|&(_, len, is_scalar)| len == 1 && is_scalar)
148            .unwrap_or(false)
149    {
150        dims.pop();
151    }
152    let out: Vec<usize> = dims.into_iter().map(|(_, len, _)| len).collect();
153    if out.is_empty() {
154        vec![1, 1]
155    } else {
156        out
157    }
158}
159
160fn exact_index_from_f64(value: f64) -> Option<i64> {
161    if !value.is_finite() {
162        return None;
163    }
164    let rounded = value.round();
165    if (rounded - value).abs() > f64::EPSILON {
166        return None;
167    }
168    if rounded < i64::MIN as f64 || rounded > i64::MAX as f64 {
169        return None;
170    }
171    Some(rounded as i64)
172}
173
174pub fn build_index_plan(
175    selectors: &[SliceSelector],
176    dims: usize,
177    base_shape: &[usize],
178) -> VmResult<IndexPlan> {
179    let total_len = checked_total_len_from_shape(base_shape)?;
180    if dims == 1 {
181        let list = selectors
182            .first()
183            .cloned()
184            .unwrap_or(SliceSelector::Indices(Vec::new()));
185        let indices = match &list {
186            SliceSelector::Colon => (1..=total_len).collect::<Vec<usize>>(),
187            SliceSelector::Scalar(i) => vec![*i],
188            SliceSelector::Indices(v) => v.clone(),
189            SliceSelector::LinearIndices { values, .. } => values.clone(),
190        };
191        if indices.iter().any(|&i| i == 0 || i > total_len) {
192            return Err(mex("IndexOutOfBounds", "Index out of bounds"));
193        }
194        let zero_based: Vec<u32> = indices
195            .iter()
196            .map(|&i| checked_u32_index(i - 1))
197            .collect::<Result<_, _>>()?;
198        let count = zero_based.len();
199        let base_is_row_vector = base_shape.first().copied().unwrap_or(1) == 1
200            && base_shape.get(1).copied().unwrap_or(1) > 1;
201        let shape = match list {
202            SliceSelector::Colon => vec![count, 1],
203            SliceSelector::LinearIndices { output_shape, .. } => output_shape,
204            _ if count == 0 => vec![0, 1],
205            _ if count <= 1 => vec![1, 1],
206            _ if base_is_row_vector => vec![1, count],
207            _ => vec![count, 1],
208        };
209        return Ok(IndexPlan::new(
210            zero_based,
211            shape,
212            vec![count],
213            dims,
214            base_shape.to_vec(),
215        ));
216    }
217
218    let mut selection_lengths = Vec::with_capacity(dims);
219    let mut per_dim_lists: Vec<Vec<usize>> = Vec::with_capacity(dims);
220    let mut scalar_mask: Vec<bool> = Vec::with_capacity(dims);
221    for (d, sel) in selectors.iter().enumerate().take(dims) {
222        let dim_len = base_shape.get(d).copied().unwrap_or(1);
223        let idxs = match sel {
224            SliceSelector::Colon => (1..=dim_len).collect::<Vec<usize>>(),
225            SliceSelector::Scalar(i) => vec![*i],
226            SliceSelector::Indices(v) => v.clone(),
227            SliceSelector::LinearIndices { values: v, .. } => v.clone(),
228        };
229        if idxs.iter().any(|&i| i == 0 || i > dim_len) {
230            return Err(mex("IndexOutOfBounds", "Index out of bounds"));
231        }
232        selection_lengths.push(idxs.len());
233        per_dim_lists.push(idxs);
234        scalar_mask.push(matches!(sel, SliceSelector::Scalar(_)));
235    }
236
237    let mut out_shape = matlab_squeezed_shape(&selection_lengths, &scalar_mask);
238    if selection_lengths.contains(&0) {
239        let selection_lengths = out_shape.clone();
240        return Ok(IndexPlan::new(
241            Vec::new(),
242            out_shape,
243            selection_lengths,
244            dims,
245            base_shape.to_vec(),
246        ));
247    }
248
249    let mut base_norm = base_shape.to_vec();
250    if base_norm.len() < dims {
251        base_norm.resize(dims, 1);
252    }
253    let mut strides = vec![1usize; dims];
254    for d in 1..dims {
255        strides[d] = strides[d - 1]
256            .checked_mul(base_norm[d - 1].max(1))
257            .ok_or_else(|| mex("IndexOutOfBounds", "Index dimensions overflow"))?;
258    }
259
260    let mut indices = Vec::new();
261    let mut index_error: Option<RuntimeError> = None;
262    cartesian_product(&per_dim_lists, |multi| {
263        if index_error.is_some() {
264            return;
265        }
266        let mut lin = 0usize;
267        for d in 0..dims {
268            let idx = multi[d] - 1;
269            let offset = match idx.checked_mul(strides[d]) {
270                Some(offset) => offset,
271                None => {
272                    index_error = Some(mex("IndexOutOfBounds", "Index dimensions overflow"));
273                    return;
274                }
275            };
276            lin = match lin.checked_add(offset) {
277                Some(sum) => sum,
278                None => {
279                    index_error = Some(mex("IndexOutOfBounds", "Index dimensions overflow"));
280                    return;
281                }
282            };
283        }
284        match checked_u32_index(lin) {
285            Ok(index) => indices.push(index),
286            Err(err) => index_error = Some(err),
287        }
288    });
289    if let Some(err) = index_error {
290        return Err(err);
291    }
292
293    let total_out: usize = selection_lengths.iter().product();
294    if total_out == 1 {
295        out_shape = vec![1, 1];
296    }
297    let selection_lengths = out_shape.clone();
298    Ok(IndexPlan::new(
299        indices,
300        out_shape,
301        selection_lengths,
302        dims,
303        base_shape.to_vec(),
304    ))
305}
306
307/// Builds a write plan for sparse two-subscript indexed assignment. The plan's
308/// base shape is the target shape so CSC updates use the expanded stride, while
309/// colon selectors are materialized against the original shape before planning.
310pub fn build_sparse_assignment_plan(
311    selectors: &[SliceSelector],
312    dims: usize,
313    base_shape: &[usize],
314) -> VmResult<IndexPlan> {
315    if dims != 2 {
316        return build_index_plan(selectors, dims, base_shape);
317    }
318
319    let mut target_shape = base_shape.to_vec();
320    target_shape.resize(dims, 1);
321    let mut planned_selectors = Vec::with_capacity(dims);
322    for (d, target_len) in target_shape.iter_mut().enumerate().take(dims) {
323        let original_len = base_shape.get(d).copied().unwrap_or(1);
324        let selector = selectors
325            .get(d)
326            .cloned()
327            .unwrap_or(SliceSelector::Indices(Vec::new()));
328        let values = match &selector {
329            SliceSelector::Colon => (1..=original_len).collect::<Vec<_>>(),
330            SliceSelector::Scalar(value) => vec![*value],
331            SliceSelector::Indices(values) => values.clone(),
332            SliceSelector::LinearIndices { values, .. } => values.clone(),
333        };
334        if values.contains(&0) {
335            return Err(mex("IndexOutOfBounds", "Index out of bounds"));
336        }
337        if let Some(&max_value) = values.iter().max() {
338            *target_len = (*target_len).max(max_value);
339        }
340        planned_selectors.push(match selector {
341            SliceSelector::Colon => SliceSelector::Indices(values),
342            other => other,
343        });
344    }
345    build_index_plan(&planned_selectors, dims, &target_shape)
346}
347
348#[derive(Clone)]
349enum ExprSel {
350    Colon,
351    Scalar(usize),
352    Indices(Vec<usize>),
353    Range {
354        start: i64,
355        step: i64,
356        end_off: EndExpr,
357    },
358}
359
360pub struct ExprPlanSpec<'a> {
361    pub dims: usize,
362    pub colon_mask: u32,
363    pub end_mask: u32,
364    pub range_dims: &'a [usize],
365    pub range_params: &'a [(f64, f64)],
366    pub range_start_exprs: &'a [Option<EndExpr>],
367    pub range_step_exprs: &'a [Option<EndExpr>],
368    pub range_end_exprs: &'a [EndExpr],
369    pub numeric: &'a [Value],
370    pub shape: &'a [usize],
371}
372
373fn selector_mask_has_dim(mask: u32, dim: usize) -> bool {
374    dim < u32::BITS as usize && (mask & (1u32 << dim)) != 0
375}
376
377fn validate_expr_range_selector_plan(
378    spec: &ExprPlanSpec<'_>,
379) -> Result<Vec<Option<usize>>, RuntimeError> {
380    let range_len = spec.range_dims.len();
381    if spec.range_params.len() != range_len
382        || spec.range_start_exprs.len() != range_len
383        || spec.range_step_exprs.len() != range_len
384        || spec.range_end_exprs.len() != range_len
385    {
386        return Err(mex(
387            "InvalidRangeSelectorPlan",
388            "inconsistent range selector metadata",
389        ));
390    }
391
392    let mut by_dim = vec![None; spec.dims];
393    for (pos, &dim) in spec.range_dims.iter().enumerate() {
394        if dim >= spec.dims {
395            return Err(mex(
396                "InvalidRangeSelectorDim",
397                "range selector dimension is out of bounds",
398            ));
399        }
400        let conflicts_with_colon = selector_mask_has_dim(spec.colon_mask, dim);
401        let conflicts_with_end = selector_mask_has_dim(spec.end_mask, dim);
402        if conflicts_with_colon || conflicts_with_end {
403            return Err(mex(
404                "InvalidRangeSelectorPlan",
405                "range selector conflicts with colon/end selector masks",
406            ));
407        }
408        if by_dim[dim].replace(pos).is_some() {
409            return Err(mex(
410                "InvalidRangeSelectorPlan",
411                "range selector dimension appears more than once",
412            ));
413        }
414    }
415    Ok(by_dim)
416}
417
418pub async fn build_expr_index_plan<ResolveEnd, Fut>(
419    spec: ExprPlanSpec<'_>,
420    resolve_end: ResolveEnd,
421) -> Result<IndexPlan, RuntimeError>
422where
423    ResolveEnd: FnMut(usize, &EndExpr) -> Fut,
424    Fut: Future<Output = Result<f64, RuntimeError>>,
425{
426    build_expr_index_plan_with_growth(spec, resolve_end, false).await
427}
428
429pub async fn build_expr_sparse_assignment_plan<ResolveEnd, Fut>(
430    spec: ExprPlanSpec<'_>,
431    resolve_end: ResolveEnd,
432) -> Result<IndexPlan, RuntimeError>
433where
434    ResolveEnd: FnMut(usize, &EndExpr) -> Fut,
435    Fut: Future<Output = Result<f64, RuntimeError>>,
436{
437    build_expr_index_plan_with_growth(spec, resolve_end, true).await
438}
439
440async fn build_expr_index_plan_with_growth<ResolveEnd, Fut>(
441    spec: ExprPlanSpec<'_>,
442    mut resolve_end: ResolveEnd,
443    allow_sparse_growth: bool,
444) -> Result<IndexPlan, RuntimeError>
445where
446    ResolveEnd: FnMut(usize, &EndExpr) -> Fut,
447    Fut: Future<Output = Result<f64, RuntimeError>>,
448{
449    let allow_sparse_growth = allow_sparse_growth && spec.dims == 2;
450    let rank = spec.shape.len();
451    let full_shape: Vec<usize> = if spec.dims == 1 {
452        vec![checked_total_len_from_shape(spec.shape)?]
453    } else if rank < spec.dims {
454        let mut s = spec.shape.to_vec();
455        s.resize(spec.dims, 1);
456        s
457    } else {
458        spec.shape.to_vec()
459    };
460
461    let range_pos_by_dim = validate_expr_range_selector_plan(&spec)?;
462    let mut selectors: Vec<ExprSel> = Vec::with_capacity(spec.dims);
463    let mut linear_output_shape: Option<Vec<usize>> = None;
464    let mut num_iter = 0usize;
465    for (d, range_pos) in range_pos_by_dim.iter().enumerate().take(spec.dims) {
466        let is_colon = selector_mask_has_dim(spec.colon_mask, d);
467        let is_end = selector_mask_has_dim(spec.end_mask, d);
468        if is_colon {
469            selectors.push(ExprSel::Colon);
470        } else if is_end {
471            selectors.push(ExprSel::Scalar(*full_shape.get(d).unwrap_or(&1)));
472        } else if let Some(pos) = *range_pos {
473            let (raw_st, raw_sp) = spec.range_params[pos];
474            let dim_len = *full_shape.get(d).unwrap_or(&1);
475            let st = if let Some(expr) = &spec.range_start_exprs[pos] {
476                resolve_end(dim_len, expr).await? as f64
477            } else {
478                raw_st
479            };
480            let sp = if let Some(expr) = &spec.range_step_exprs[pos] {
481                resolve_end(dim_len, expr).await? as f64
482            } else {
483                raw_sp
484            };
485            let start = exact_index_from_f64(st).ok_or_else(|| {
486                mex(
487                    "UnsupportedIndexType",
488                    "Index values must be positive integers or logical values",
489                )
490            })?;
491            let step = exact_index_from_f64(sp).ok_or_else(|| {
492                mex(
493                    "UnsupportedIndexType",
494                    "Index values must be positive integers or logical values",
495                )
496            })?;
497            let off = spec.range_end_exprs[pos].clone();
498            selectors.push(ExprSel::Range {
499                start,
500                step,
501                end_off: off,
502            });
503        } else {
504            let v = spec
505                .numeric
506                .get(num_iter)
507                .ok_or_else(|| mex("MissingNumericIndex", "missing numeric index"))?;
508            num_iter += 1;
509            if let Some(idx) = index_scalar_from_value(v).await? {
510                if idx.is_below_one() {
511                    return Err(mex("IndexOutOfBounds", "Index out of bounds"));
512                }
513                let index = idx
514                    .positive_usize()
515                    .ok_or_else(|| mex("IndexOutOfBounds", "Index out of bounds"))?;
516                selectors.push(ExprSel::Scalar(index));
517            } else {
518                // Vector selectors may be GPU-backed. Materialize them before
519                // inspecting their class so typed integer storage remains the
520                // source of truth instead of a floating-point compatibility view.
521                let materialized = materialize_index_value(v).await?;
522                let v = &materialized;
523                match v {
524                    Value::Bool(b) => {
525                        selectors.push(if *b {
526                            ExprSel::Indices(vec![1])
527                        } else {
528                            ExprSel::Indices(Vec::new())
529                        });
530                    }
531                    Value::LogicalArray(la) => {
532                        if la.data.len() == 1 && is_scalar_shape(&la.shape) {
533                            selectors.push(if la.data[0] != 0 {
534                                ExprSel::Indices(vec![1])
535                            } else {
536                                ExprSel::Indices(Vec::new())
537                            });
538                        } else {
539                            let dim_len = *full_shape.get(d).unwrap_or(&1);
540                            if spec.dims == 1 {
541                                let vv = logical_indices_linear(la, dim_len)?;
542                                // MATLAB-style linear logical indexing returns a column vector.
543                                linear_output_shape = Some(vec![vv.len(), 1]);
544                                selectors.push(ExprSel::Indices(vv));
545                            } else {
546                                if la.data.len() != dim_len {
547                                    return Err(mex(
548                                        "IndexShape",
549                                        "Logical mask length mismatch for dimension",
550                                    ));
551                                }
552                                let vv = la
553                                    .data
554                                    .iter()
555                                    .enumerate()
556                                    .filter_map(|(index, &selected)| {
557                                        (selected != 0).then_some(index + 1)
558                                    })
559                                    .collect();
560                                selectors.push(ExprSel::Indices(vv));
561                            }
562                        }
563                    }
564                    Value::Tensor(idx_t) => {
565                        if spec.dims == 1 {
566                            linear_output_shape = Some(idx_t.shape.clone());
567                        }
568                        let vv = numeric_tensor_indices(idx_t, None)?;
569                        selectors.push(ExprSel::Indices(vv));
570                    }
571                    _ => return Err(mex("UnsupportedIndexType", "Unsupported index type")),
572                }
573            }
574        }
575    }
576
577    let mut per_dim_indices: Vec<Vec<usize>> = Vec::with_capacity(spec.dims);
578    let mut selection_lengths: Vec<usize> = Vec::with_capacity(spec.dims);
579    let mut scalar_mask: Vec<bool> = Vec::with_capacity(spec.dims);
580    let base_is_row_vector = spec.dims == 1
581        && spec.shape.first().copied().unwrap_or(1) == 1
582        && spec.shape.get(1).copied().unwrap_or(1) > 1;
583    let linear_selector_is_colon = matches!(selectors.first(), Some(ExprSel::Colon));
584    let linear_selector_is_range = matches!(selectors.first(), Some(ExprSel::Range { .. }));
585    for (d, sel) in selectors.iter().enumerate().take(spec.dims) {
586        let dim_len = full_shape[d] as i64;
587        let idxs: Vec<usize> = match sel {
588            ExprSel::Colon => (1..=full_shape[d]).collect(),
589            ExprSel::Scalar(i) => vec![*i],
590            ExprSel::Indices(v) => v.clone(),
591            ExprSel::Range {
592                start,
593                step,
594                end_off,
595            } => {
596                let mut v = Vec::new();
597                let mut cur = *start;
598                let stp = *step;
599                let end_bound = resolve_end(dim_len as usize, end_off).await?;
600                if stp == 0 {
601                    return Err(mex("IndexStepZero", "Index step cannot be zero"));
602                }
603                if !end_bound.is_finite() {
604                    return Err(mex(
605                        "UnsupportedIndexType",
606                        "Index values must be positive integers or logical values",
607                    ));
608                }
609                let end_i = if stp > 0 {
610                    end_bound.floor()
611                } else {
612                    end_bound.ceil()
613                };
614                if end_i < i64::MIN as f64 || end_i > i64::MAX as f64 {
615                    return Err(mex(
616                        "UnsupportedIndexType",
617                        "Index values must be positive integers or logical values",
618                    ));
619                }
620                let end_i = end_i as i64;
621                if stp > 0 {
622                    while cur <= end_i {
623                        if cur < 1 || (!allow_sparse_growth && cur > dim_len) {
624                            return Err(mex("IndexOutOfBounds", "Index out of bounds"));
625                        }
626                        v.push(cur as usize);
627                        cur += stp;
628                    }
629                } else {
630                    while cur >= end_i {
631                        if cur < 1 || (!allow_sparse_growth && cur > dim_len) {
632                            return Err(mex("IndexOutOfBounds", "Index out of bounds"));
633                        }
634                        v.push(cur as usize);
635                        cur += stp;
636                    }
637                }
638                v
639            }
640        };
641        if idxs
642            .iter()
643            .any(|&i| i == 0 || (!allow_sparse_growth && i > full_shape[d]))
644        {
645            return Err(mex("IndexOutOfBounds", "Index out of bounds"));
646        }
647        selection_lengths.push(idxs.len());
648        per_dim_indices.push(idxs);
649        scalar_mask.push(matches!(sel, ExprSel::Scalar(_)));
650    }
651
652    let mut planned_shape = full_shape.clone();
653    if allow_sparse_growth && spec.dims > 1 {
654        for (d, indices) in per_dim_indices.iter().enumerate().take(spec.dims) {
655            if let Some(&max_index) = indices.iter().max() {
656                planned_shape[d] = planned_shape[d].max(max_index);
657            }
658        }
659    }
660    let mut strides: Vec<usize> = vec![0; spec.dims];
661    let mut acc = 1usize;
662    for (d, stride) in strides.iter_mut().enumerate().take(spec.dims) {
663        *stride = acc;
664        acc = acc
665            .checked_mul(planned_shape[d])
666            .ok_or_else(|| mex("IndexOutOfBounds", "Index dimensions overflow"))?;
667    }
668    let total_out: usize = per_dim_indices.iter().try_fold(1usize, |acc, values| {
669        acc.checked_mul(values.len())
670            .ok_or_else(|| mex("IndexOutOfBounds", "Index result dimensions overflow"))
671    })?;
672    if total_out == 0 {
673        let output_shape = if spec.dims == 1 {
674            if let Some(shape) = linear_output_shape.clone() {
675                shape
676            } else if linear_selector_is_colon {
677                vec![0, 1]
678            } else if linear_selector_is_range || base_is_row_vector {
679                vec![1, 0]
680            } else {
681                vec![0, 1]
682            }
683        } else {
684            let mut dims_out: Vec<(usize, usize, bool)> = selection_lengths
685                .iter()
686                .enumerate()
687                .map(|(d, &len)| (d, len, scalar_mask.get(d).copied().unwrap_or(false)))
688                .collect();
689            while dims_out.len() > 2
690                && dims_out
691                    .last()
692                    .map(|&(_, len, is_scalar)| len == 1 && is_scalar)
693                    .unwrap_or(false)
694            {
695                dims_out.pop();
696            }
697            if dims_out.is_empty() {
698                vec![1, 1]
699            } else if dims_out.len() == 1 {
700                let (dim, len, _) = dims_out[0];
701                if dim == 1 {
702                    vec![1, len]
703                } else {
704                    vec![len, 1]
705                }
706            } else {
707                dims_out.into_iter().map(|(_, len, _)| len).collect()
708            }
709        };
710        return Ok(IndexPlan::new(
711            Vec::new(),
712            output_shape,
713            selection_lengths,
714            spec.dims,
715            planned_shape,
716        ));
717    }
718
719    let mut indices: Vec<u32> = Vec::with_capacity(total_out);
720    let mut idx = vec![0usize; spec.dims];
721    loop {
722        let mut lin = 0usize;
723        for d in 0..spec.dims {
724            let i0 = per_dim_indices[d][idx[d]] - 1;
725            let offset = i0
726                .checked_mul(strides[d])
727                .ok_or_else(|| mex("IndexOutOfBounds", "Index dimensions overflow"))?;
728            lin = lin
729                .checked_add(offset)
730                .ok_or_else(|| mex("IndexOutOfBounds", "Index dimensions overflow"))?;
731        }
732        indices.push(checked_u32_index(lin)?);
733        let mut d = 0usize;
734        while d < spec.dims {
735            idx[d] += 1;
736            if idx[d] < per_dim_indices[d].len() {
737                break;
738            }
739            idx[d] = 0;
740            d += 1;
741        }
742        if d == spec.dims {
743            break;
744        }
745    }
746
747    let output_shape = if spec.dims == 1 {
748        if let Some(shape) = linear_output_shape {
749            shape
750        } else if total_out <= 1 {
751            vec![1, 1]
752        } else if linear_selector_is_colon {
753            vec![total_out, 1]
754        } else if linear_selector_is_range || base_is_row_vector {
755            vec![1, total_out]
756        } else {
757            vec![total_out, 1]
758        }
759    } else {
760        let mut dims_out: Vec<(usize, usize, bool)> = selection_lengths
761            .iter()
762            .enumerate()
763            .map(|(d, &len)| (d, len, scalar_mask.get(d).copied().unwrap_or(false)))
764            .collect();
765        while dims_out.len() > 2
766            && dims_out
767                .last()
768                .map(|&(_, len, is_scalar)| len == 1 && is_scalar)
769                .unwrap_or(false)
770        {
771            dims_out.pop();
772        }
773        if dims_out.is_empty() {
774            vec![1, 1]
775        } else if dims_out.len() == 1 {
776            let (dim, len, _) = dims_out[0];
777            if dim == 1 {
778                vec![1, len]
779            } else {
780                vec![len, 1]
781            }
782        } else {
783            dims_out.into_iter().map(|(_, len, _)| len).collect()
784        }
785    };
786    Ok(IndexPlan::new(
787        indices,
788        output_shape,
789        selection_lengths,
790        spec.dims,
791        planned_shape,
792    ))
793}
794
795#[cfg(test)]
796mod tests {
797    use super::{
798        build_expr_index_plan, build_index_plan, build_sparse_assignment_plan, ExprPlanSpec,
799    };
800    use crate::indexing::selectors::{build_slice_selectors, SliceSelector};
801    use crate::indexing::EndExpr;
802    use runmat_value::{IntegerStorage, LogicalArray, Tensor, Value};
803
804    #[test]
805    fn sparse_assignment_plan_expands_numeric_dimensions_but_keeps_colon_at_old_extent() {
806        let selectors = vec![SliceSelector::Indices(vec![3, 4]), SliceSelector::Colon];
807        let plan = build_sparse_assignment_plan(&selectors, 2, &[2, 2])
808            .expect("sparse assignment plan should grow rows");
809        assert_eq!(plan.base_shape, vec![4, 2]);
810        assert_eq!(plan.selection_lengths, vec![2, 2]);
811        assert_eq!(plan.indices, vec![2, 3, 6, 7]);
812    }
813
814    #[test]
815    fn plain_and_expr_linear_range_plans_match() {
816        futures::executor::block_on(async {
817            let shape = vec![1, 10];
818            let numeric = vec![Value::Tensor(
819                Tensor::new(vec![2.0, 4.0, 6.0, 8.0], vec![1, 4]).unwrap(),
820            )];
821            let plain_selectors = build_slice_selectors(1, 0, 0, &numeric, &shape)
822                .await
823                .unwrap();
824            let plain = build_index_plan(&plain_selectors, 1, &shape).unwrap();
825            let expr = build_expr_index_plan(
826                ExprPlanSpec {
827                    dims: 1,
828                    colon_mask: 0,
829                    end_mask: 0,
830                    range_dims: &[0],
831                    range_params: &[(2.0, 2.0)],
832                    range_start_exprs: &[None],
833                    range_step_exprs: &[None],
834                    range_end_exprs: &[EndExpr::Sub(
835                        Box::new(EndExpr::End),
836                        Box::new(EndExpr::Const(1.0)),
837                    )],
838                    numeric: &[],
839                    shape: &shape,
840                },
841                |dim_len, expr| {
842                    let expr = expr.clone();
843                    async move {
844                        Ok(match &expr {
845                            EndExpr::End => dim_len as f64,
846                            EndExpr::Const(value) => *value,
847                            EndExpr::Sub(lhs, rhs) => {
848                                let lhs_val = match lhs.as_ref() {
849                                    EndExpr::End => dim_len as f64,
850                                    EndExpr::Const(value) => *value,
851                                    other => panic!("unsupported lhs expr: {other:?}"),
852                                };
853                                let rhs_val = match rhs.as_ref() {
854                                    EndExpr::Const(value) => *value,
855                                    other => panic!("unsupported rhs expr: {other:?}"),
856                                };
857                                lhs_val - rhs_val
858                            }
859                            other => panic!("unsupported expr: {other:?}"),
860                        })
861                    }
862                },
863            )
864            .await
865            .unwrap();
866            assert_eq!(plain.indices, expr.indices);
867            assert_eq!(plain.output_shape, expr.output_shape);
868            assert_eq!(plain.selection_lengths, expr.selection_lengths);
869            assert_eq!(plain.properties.full_row, expr.properties.full_row);
870            assert_eq!(plain.properties.full_column, expr.properties.full_column);
871        })
872    }
873
874    #[test]
875    fn expr_integer_index_vectors_use_exact_storage_for_all_classes() {
876        macro_rules! assert_indices {
877            ($storage:expr) => {{
878                let indices =
879                    Tensor::new_integer($storage, vec![1, 2]).expect("typed integer index tensor");
880                let numeric = vec![Value::Tensor(indices)];
881                let plan = futures::executor::block_on(build_expr_index_plan(
882                    ExprPlanSpec {
883                        dims: 1,
884                        colon_mask: 0,
885                        end_mask: 0,
886                        range_dims: &[],
887                        range_params: &[],
888                        range_start_exprs: &[],
889                        range_step_exprs: &[],
890                        range_end_exprs: &[],
891                        numeric: &numeric,
892                        shape: &[1, 2],
893                    },
894                    |_, _| async { Ok(0.0) },
895                ))
896                .expect("exact integer vector index plan");
897                assert_eq!(plan.indices, vec![0, 1]);
898            }};
899        }
900
901        assert_indices!(IntegerStorage::I8(vec![1, 2]));
902        assert_indices!(IntegerStorage::I16(vec![1, 2]));
903        assert_indices!(IntegerStorage::I32(vec![1, 2]));
904        assert_indices!(IntegerStorage::I64(vec![1, 2]));
905        assert_indices!(IntegerStorage::U8(vec![1, 2]));
906        assert_indices!(IntegerStorage::U16(vec![1, 2]));
907        assert_indices!(IntegerStorage::U32(vec![1, 2]));
908        assert_indices!(IntegerStorage::U64(vec![1, 2]));
909    }
910
911    #[test]
912    fn expr_integer_index_vectors_reject_wide_values() {
913        let indices = Tensor::new_integer(IntegerStorage::U64(vec![1, u64::MAX]), vec![1, 2])
914            .expect("typed integer index tensor");
915        let numeric = vec![Value::Tensor(indices)];
916
917        let err = futures::executor::block_on(build_expr_index_plan(
918            ExprPlanSpec {
919                dims: 1,
920                colon_mask: 0,
921                end_mask: 0,
922                range_dims: &[],
923                range_params: &[],
924                range_start_exprs: &[],
925                range_step_exprs: &[],
926                range_end_exprs: &[],
927                numeric: &numeric,
928                shape: &[1, 2],
929            },
930            |_, _| async { Ok(0.0) },
931        ))
932        .expect_err("wide exact integer index must not use its float mirror");
933        assert_eq!(err.identifier(), Some("RunMat:IndexOutOfBounds"));
934    }
935
936    #[test]
937    fn plain_and_expr_column_plans_match_properties() {
938        futures::executor::block_on(async {
939            let shape = vec![3, 4];
940            let numeric = vec![Value::Num(3.0)];
941            let plain_selectors = build_slice_selectors(2, 1, 0, &numeric, &shape)
942                .await
943                .unwrap();
944            let plain = build_index_plan(&plain_selectors, 2, &shape).unwrap();
945            let expr = build_expr_index_plan(
946                ExprPlanSpec {
947                    dims: 2,
948                    colon_mask: 1,
949                    end_mask: 0,
950                    range_dims: &[],
951                    range_params: &[],
952                    range_start_exprs: &[],
953                    range_step_exprs: &[],
954                    range_end_exprs: &[],
955                    numeric: &numeric,
956                    shape: &shape,
957                },
958                |_dim_len, _expr| async move { unreachable!() },
959            )
960            .await
961            .unwrap();
962            assert_eq!(plain.indices, expr.indices);
963            assert_eq!(plain.properties.full_column, Some(2));
964            assert_eq!(plain.properties.full_column, expr.properties.full_column);
965            assert_eq!(plain.properties.full_row, expr.properties.full_row);
966        })
967    }
968
969    #[test]
970    fn expr_linear_range_on_column_vector_uses_range_shape() {
971        futures::executor::block_on(async {
972            let plan = build_expr_index_plan(
973                ExprPlanSpec {
974                    dims: 1,
975                    colon_mask: 0,
976                    end_mask: 0,
977                    range_dims: &[0],
978                    range_params: &[(1.0, 1.0)],
979                    range_start_exprs: &[None],
980                    range_step_exprs: &[None],
981                    range_end_exprs: &[EndExpr::Var(0)],
982                    numeric: &[],
983                    shape: &[10, 1],
984                },
985                |_dim_len, expr| {
986                    let expr = expr.clone();
987                    async move {
988                        match expr {
989                            EndExpr::Var(_) => Ok(6.0),
990                            other => panic!("unsupported expr: {other:?}"),
991                        }
992                    }
993                },
994            )
995            .await
996            .unwrap();
997            assert_eq!(plan.indices, vec![0, 1, 2, 3, 4, 5]);
998            assert_eq!(plan.output_shape, vec![1, 6]);
999            assert_eq!(plan.selection_lengths, vec![6]);
1000        })
1001    }
1002
1003    #[test]
1004    fn expr_empty_linear_range_uses_range_shape() {
1005        futures::executor::block_on(async {
1006            let plan = build_expr_index_plan(
1007                ExprPlanSpec {
1008                    dims: 1,
1009                    colon_mask: 0,
1010                    end_mask: 0,
1011                    range_dims: &[0],
1012                    range_params: &[(1.0, 1.0)],
1013                    range_start_exprs: &[None],
1014                    range_step_exprs: &[None],
1015                    range_end_exprs: &[EndExpr::Var(0)],
1016                    numeric: &[],
1017                    shape: &[10, 1],
1018                },
1019                |_dim_len, expr| {
1020                    let expr = expr.clone();
1021                    async move {
1022                        match expr {
1023                            EndExpr::Var(_) => Ok(0.0),
1024                            other => panic!("unsupported expr: {other:?}"),
1025                        }
1026                    }
1027                },
1028            )
1029            .await
1030            .unwrap();
1031            assert!(plan.indices.is_empty());
1032            assert_eq!(plan.output_shape, vec![1, 0]);
1033            assert_eq!(plan.selection_lengths, vec![0]);
1034        })
1035    }
1036
1037    #[test]
1038    fn expr_plan_rejects_range_dim_conflicting_with_colon_mask() {
1039        futures::executor::block_on(async {
1040            let err = build_expr_index_plan(
1041                ExprPlanSpec {
1042                    dims: 2,
1043                    colon_mask: 0b01,
1044                    end_mask: 0,
1045                    range_dims: &[0],
1046                    range_params: &[(1.0, 1.0)],
1047                    range_start_exprs: &[None],
1048                    range_step_exprs: &[None],
1049                    range_end_exprs: &[EndExpr::End],
1050                    numeric: &[Value::Num(1.0)],
1051                    shape: &[3, 3],
1052                },
1053                |_dim_len, _expr| async move { unreachable!() },
1054            )
1055            .await
1056            .expect_err("range/colon conflict should fail");
1057            assert_eq!(err.identifier(), Some("RunMat:InvalidRangeSelectorPlan"));
1058        })
1059    }
1060
1061    #[test]
1062    fn expr_plan_rejects_range_dim_conflicting_with_end_mask() {
1063        futures::executor::block_on(async {
1064            let err = build_expr_index_plan(
1065                ExprPlanSpec {
1066                    dims: 2,
1067                    colon_mask: 0,
1068                    end_mask: 0b10,
1069                    range_dims: &[1],
1070                    range_params: &[(1.0, 1.0)],
1071                    range_start_exprs: &[None],
1072                    range_step_exprs: &[None],
1073                    range_end_exprs: &[EndExpr::End],
1074                    numeric: &[Value::Num(1.0)],
1075                    shape: &[3, 3],
1076                },
1077                |_dim_len, _expr| async move { unreachable!() },
1078            )
1079            .await
1080            .expect_err("range/end conflict should fail");
1081            assert_eq!(err.identifier(), Some("RunMat:InvalidRangeSelectorPlan"));
1082        })
1083    }
1084
1085    #[test]
1086    fn expr_plan_rejects_duplicate_range_dims() {
1087        futures::executor::block_on(async {
1088            let err = build_expr_index_plan(
1089                ExprPlanSpec {
1090                    dims: 2,
1091                    colon_mask: 0,
1092                    end_mask: 0,
1093                    range_dims: &[1, 1],
1094                    range_params: &[(1.0, 1.0), (1.0, 1.0)],
1095                    range_start_exprs: &[None, None],
1096                    range_step_exprs: &[None, None],
1097                    range_end_exprs: &[EndExpr::End, EndExpr::End],
1098                    numeric: &[Value::Num(1.0)],
1099                    shape: &[3, 3],
1100                },
1101                |_dim_len, _expr| async move { unreachable!() },
1102            )
1103            .await
1104            .expect_err("duplicate range dims should fail");
1105            assert_eq!(err.identifier(), Some("RunMat:InvalidRangeSelectorPlan"));
1106        })
1107    }
1108
1109    #[test]
1110    fn expr_plan_rejects_out_of_bounds_range_dim() {
1111        futures::executor::block_on(async {
1112            let err = build_expr_index_plan(
1113                ExprPlanSpec {
1114                    dims: 2,
1115                    colon_mask: 0,
1116                    end_mask: 0,
1117                    range_dims: &[2],
1118                    range_params: &[(1.0, 1.0)],
1119                    range_start_exprs: &[None],
1120                    range_step_exprs: &[None],
1121                    range_end_exprs: &[EndExpr::End],
1122                    numeric: &[Value::Num(1.0), Value::Num(1.0)],
1123                    shape: &[3, 3],
1124                },
1125                |_dim_len, _expr| async move { unreachable!() },
1126            )
1127            .await
1128            .expect_err("out-of-bounds range dim should fail");
1129            assert_eq!(err.identifier(), Some("RunMat:InvalidRangeSelectorDim"));
1130        })
1131    }
1132
1133    #[test]
1134    fn expr_plan_rejects_inconsistent_range_metadata_lengths() {
1135        futures::executor::block_on(async {
1136            let err = build_expr_index_plan(
1137                ExprPlanSpec {
1138                    dims: 2,
1139                    colon_mask: 0,
1140                    end_mask: 0,
1141                    range_dims: &[1],
1142                    range_params: &[],
1143                    range_start_exprs: &[None],
1144                    range_step_exprs: &[None],
1145                    range_end_exprs: &[EndExpr::End],
1146                    numeric: &[Value::Num(1.0)],
1147                    shape: &[3, 3],
1148                },
1149                |_dim_len, _expr| async move { unreachable!() },
1150            )
1151            .await
1152            .expect_err("inconsistent range metadata should fail");
1153            assert_eq!(err.identifier(), Some("RunMat:InvalidRangeSelectorPlan"));
1154        })
1155    }
1156
1157    #[test]
1158    #[cfg(target_pointer_width = "64")]
1159    fn index_plan_rejects_sparse_sized_indices_beyond_u32_storage() {
1160        let selectors = vec![SliceSelector::Scalar((u32::MAX as usize) + 2)];
1161        let err = build_index_plan(&selectors, 1, &[u32::MAX as usize + 2, 1])
1162            .expect_err("linear index should exceed u32 plan storage");
1163        assert_eq!(err.identifier(), Some("RunMat:IndexOutOfBounds"));
1164    }
1165
1166    #[test]
1167    fn index_plan_rejects_dimension_product_overflow() {
1168        let selectors = vec![SliceSelector::Colon];
1169        let err = build_index_plan(&selectors, 1, &[usize::MAX, 2])
1170            .expect_err("linearized sparse shape should overflow");
1171        assert_eq!(err.identifier(), Some("RunMat:IndexOutOfBounds"));
1172    }
1173
1174    #[test]
1175    fn expr_plan_supports_dims_beyond_mask_width() {
1176        futures::executor::block_on(async {
1177            let numeric = vec![Value::Num(1.0); 31];
1178            let shape = vec![1usize; 33];
1179            let plan = build_expr_index_plan(
1180                ExprPlanSpec {
1181                    dims: 33,
1182                    colon_mask: 0b1,
1183                    end_mask: 0b10,
1184                    range_dims: &[],
1185                    range_params: &[],
1186                    range_start_exprs: &[],
1187                    range_step_exprs: &[],
1188                    range_end_exprs: &[],
1189                    numeric: &numeric,
1190                    shape: &shape,
1191                },
1192                |_dim_len, _expr| async move { unreachable!() },
1193            )
1194            .await
1195            .expect("expr plan for dims beyond mask width");
1196            assert_eq!(plan.dims, 33);
1197            assert!(!plan.indices.is_empty());
1198        })
1199    }
1200
1201    #[test]
1202    fn expr_plan_tensor_selector_length_match_uses_numeric_indices() {
1203        futures::executor::block_on(async {
1204            let shape = vec![3, 2];
1205            let numeric = vec![Value::Tensor(
1206                Tensor::new(vec![2.0, 1.0, 3.0], vec![1, 3]).expect("selector tensor"),
1207            )];
1208            let plain_selectors = build_slice_selectors(2, 0b10, 0, &numeric, &shape)
1209                .await
1210                .unwrap();
1211            let plain = build_index_plan(&plain_selectors, 2, &shape).unwrap();
1212            let expr = build_expr_index_plan(
1213                ExprPlanSpec {
1214                    dims: 2,
1215                    colon_mask: 0b10,
1216                    end_mask: 0,
1217                    range_dims: &[],
1218                    range_params: &[],
1219                    range_start_exprs: &[],
1220                    range_step_exprs: &[],
1221                    range_end_exprs: &[],
1222                    numeric: &numeric,
1223                    shape: &shape,
1224                },
1225                |_dim_len, _expr| async move { unreachable!() },
1226            )
1227            .await
1228            .unwrap();
1229            assert_eq!(plain.indices, expr.indices);
1230            assert_eq!(plain.output_shape, expr.output_shape);
1231            assert_eq!(plain.selection_lengths, expr.selection_lengths);
1232        })
1233    }
1234
1235    #[test]
1236    fn expr_plan_logical_selector_remains_logical_mask() {
1237        futures::executor::block_on(async {
1238            let shape = vec![3, 2];
1239            let numeric = vec![Value::LogicalArray(
1240                LogicalArray::new(vec![0, 1, 1], vec![1, 3]).expect("logical selector"),
1241            )];
1242            let plain_selectors = build_slice_selectors(2, 0b10, 0, &numeric, &shape)
1243                .await
1244                .unwrap();
1245            let plain = build_index_plan(&plain_selectors, 2, &shape).unwrap();
1246            let expr = build_expr_index_plan(
1247                ExprPlanSpec {
1248                    dims: 2,
1249                    colon_mask: 0b10,
1250                    end_mask: 0,
1251                    range_dims: &[],
1252                    range_params: &[],
1253                    range_start_exprs: &[],
1254                    range_step_exprs: &[],
1255                    range_end_exprs: &[],
1256                    numeric: &numeric,
1257                    shape: &shape,
1258                },
1259                |_dim_len, _expr| async move { unreachable!() },
1260            )
1261            .await
1262            .unwrap();
1263            assert_eq!(plain.indices, expr.indices);
1264            assert_eq!(plain.output_shape, expr.output_shape);
1265            assert_eq!(plain.selection_lengths, expr.selection_lengths);
1266        })
1267    }
1268
1269    #[test]
1270    fn expr_plan_linear_tensor_selector_preserves_tensor_shape() {
1271        futures::executor::block_on(async {
1272            let shape = vec![1, 10];
1273            let numeric = vec![Value::Tensor(
1274                Tensor::new(vec![2.0, 4.0], vec![2, 1]).expect("selector tensor"),
1275            )];
1276            let plain_selectors = build_slice_selectors(1, 0, 0, &numeric, &shape)
1277                .await
1278                .unwrap();
1279            let plain = build_index_plan(&plain_selectors, 1, &shape).unwrap();
1280            let expr = build_expr_index_plan(
1281                ExprPlanSpec {
1282                    dims: 1,
1283                    colon_mask: 0,
1284                    end_mask: 0,
1285                    range_dims: &[],
1286                    range_params: &[],
1287                    range_start_exprs: &[],
1288                    range_step_exprs: &[],
1289                    range_end_exprs: &[],
1290                    numeric: &numeric,
1291                    shape: &shape,
1292                },
1293                |_dim_len, _expr| async move { unreachable!() },
1294            )
1295            .await
1296            .unwrap();
1297            assert_eq!(plain.indices, expr.indices);
1298            assert_eq!(plain.output_shape, expr.output_shape);
1299            assert_eq!(plain.selection_lengths, expr.selection_lengths);
1300        })
1301    }
1302
1303    #[test]
1304    fn expr_plan_linear_colon_selector_matches_plain_shape() {
1305        futures::executor::block_on(async {
1306            let shape = vec![1, 5];
1307            let plain = build_index_plan(&[SliceSelector::Colon], 1, &shape).unwrap();
1308            let expr = build_expr_index_plan(
1309                ExprPlanSpec {
1310                    dims: 1,
1311                    colon_mask: 0b1,
1312                    end_mask: 0,
1313                    range_dims: &[],
1314                    range_params: &[],
1315                    range_start_exprs: &[],
1316                    range_step_exprs: &[],
1317                    range_end_exprs: &[],
1318                    numeric: &[],
1319                    shape: &shape,
1320                },
1321                |_dim_len, _expr| async move { unreachable!() },
1322            )
1323            .await
1324            .unwrap();
1325            assert_eq!(plain.indices, expr.indices);
1326            assert_eq!(plain.output_shape, expr.output_shape);
1327            assert_eq!(plain.selection_lengths, expr.selection_lengths);
1328        })
1329    }
1330
1331    #[test]
1332    fn expr_plan_linear_logical_mask_matches_plain_shape() {
1333        futures::executor::block_on(async {
1334            let shape = vec![1, 5];
1335            let numeric = vec![Value::LogicalArray(
1336                LogicalArray::new(vec![1, 0, 1, 0, 1], vec![1, 5]).expect("logical selector"),
1337            )];
1338            let plain_selectors = build_slice_selectors(1, 0, 0, &numeric, &shape)
1339                .await
1340                .unwrap();
1341            let plain = build_index_plan(&plain_selectors, 1, &shape).unwrap();
1342            let expr = build_expr_index_plan(
1343                ExprPlanSpec {
1344                    dims: 1,
1345                    colon_mask: 0,
1346                    end_mask: 0,
1347                    range_dims: &[],
1348                    range_params: &[],
1349                    range_start_exprs: &[],
1350                    range_step_exprs: &[],
1351                    range_end_exprs: &[],
1352                    numeric: &numeric,
1353                    shape: &shape,
1354                },
1355                |_dim_len, _expr| async move { unreachable!() },
1356            )
1357            .await
1358            .unwrap();
1359            assert_eq!(plain.indices, expr.indices);
1360            assert_eq!(plain.output_shape, expr.output_shape);
1361            assert_eq!(plain.selection_lengths, expr.selection_lengths);
1362        })
1363    }
1364
1365    #[test]
1366    fn expr_plan_short_linear_logical_mask_matches_plain_column_shape() {
1367        futures::executor::block_on(async {
1368            let shape = vec![2, 3];
1369            let numeric = vec![Value::LogicalArray(
1370                LogicalArray::new(vec![0, 1, 1], vec![1, 3]).expect("logical selector"),
1371            )];
1372            let plain_selectors = build_slice_selectors(1, 0, 0, &numeric, &shape)
1373                .await
1374                .unwrap();
1375            let plain = build_index_plan(&plain_selectors, 1, &shape).unwrap();
1376            let expr = build_expr_index_plan(
1377                ExprPlanSpec {
1378                    dims: 1,
1379                    colon_mask: 0,
1380                    end_mask: 0,
1381                    range_dims: &[],
1382                    range_params: &[],
1383                    range_start_exprs: &[],
1384                    range_step_exprs: &[],
1385                    range_end_exprs: &[],
1386                    numeric: &numeric,
1387                    shape: &shape,
1388                },
1389                |_dim_len, _expr| async move { unreachable!() },
1390            )
1391            .await
1392            .unwrap();
1393            assert_eq!(plain.indices, vec![1, 2]);
1394            assert_eq!(plain.indices, expr.indices);
1395            assert_eq!(plain.output_shape, vec![2, 1]);
1396            assert_eq!(plain.output_shape, expr.output_shape);
1397            assert_eq!(plain.selection_lengths, expr.selection_lengths);
1398        })
1399    }
1400
1401    #[test]
1402    fn linear_empty_selector_uses_empty_column_shape() {
1403        let plan = build_index_plan(&[SliceSelector::Indices(Vec::new())], 1, &[1, 5])
1404            .expect("empty linear selector should build");
1405        assert!(plan.indices.is_empty());
1406        assert_eq!(plan.output_shape, vec![0, 1]);
1407    }
1408
1409    #[test]
1410    fn expr_plan_linear_empty_logical_mask_matches_plain_shape() {
1411        futures::executor::block_on(async {
1412            let shape = vec![1, 5];
1413            let numeric = vec![Value::LogicalArray(
1414                LogicalArray::new(vec![0, 0, 0, 0, 0], vec![1, 5]).expect("logical selector"),
1415            )];
1416            let plain_selectors = build_slice_selectors(1, 0, 0, &numeric, &shape)
1417                .await
1418                .unwrap();
1419            let plain = build_index_plan(&plain_selectors, 1, &shape).unwrap();
1420            let expr = build_expr_index_plan(
1421                ExprPlanSpec {
1422                    dims: 1,
1423                    colon_mask: 0,
1424                    end_mask: 0,
1425                    range_dims: &[],
1426                    range_params: &[],
1427                    range_start_exprs: &[],
1428                    range_step_exprs: &[],
1429                    range_end_exprs: &[],
1430                    numeric: &numeric,
1431                    shape: &shape,
1432                },
1433                |_dim_len, _expr| async move { unreachable!() },
1434            )
1435            .await
1436            .unwrap();
1437            assert!(plain.indices.is_empty());
1438            assert!(expr.indices.is_empty());
1439            assert_eq!(plain.output_shape, vec![0, 1]);
1440            assert_eq!(plain.output_shape, expr.output_shape);
1441            assert_eq!(plain.selection_lengths, expr.selection_lengths);
1442        })
1443    }
1444}