Skip to main content

runmat_runtime/
iteration.rs

1//! Executor-neutral MATLAB `for` iteration semantics.
2//!
3//! MATLAB iterates a value by columns: the iterable is evaluated once, its
4//! second dimension determines the iteration count, and each iteration binds
5//! the equivalent of `value(:, column)`. Executors own control-flow state;
6//! this module owns the value semantics of that operation.
7
8use runmat_value::{CellArray, CharArray, LogicalArray, ObjectArray, SymbolicArray, Value};
9
10use crate::indexing::plan::{build_index_plan, IndexPlan};
11use crate::indexing::read_slice::{
12    gather_string_slice, read_complex_slice_from_plan, read_gpu_slice_from_plan,
13    read_sparse_slice_from_plan, read_tensor_slice_from_plan,
14};
15use crate::indexing::selectors::SliceSelector;
16use crate::object::dispatch::call_object_index_descriptor_method;
17use crate::object::indexing::ObjectIndexDescriptor;
18use crate::{call_builtin_async, RuntimeError};
19
20/// A snapshot of one MATLAB `for` iterable.
21///
22/// The source value is cloned once when the loop is entered. Later workspace
23/// mutation therefore cannot change the loop's remaining values.
24#[derive(Debug, Clone)]
25pub struct ForColumnIterator {
26    source: Value,
27    next_column: usize,
28    column_count: usize,
29}
30
31impl ForColumnIterator {
32    /// Capture an iterable and resolve its MATLAB-visible second dimension.
33    pub async fn new(source: Value) -> Result<Self, RuntimeError> {
34        let size = call_builtin_async("size", &[source.clone(), Value::Num(2.0)]).await?;
35        let column_count = dimension_extent(&size)?;
36        Ok(Self {
37            source,
38            next_column: 0,
39            column_count,
40        })
41    }
42
43    /// Return the next column, or `None` once the captured iterable is exhausted.
44    pub async fn next(&mut self) -> Result<Option<Value>, RuntimeError> {
45        if self.next_column >= self.column_count {
46            return Ok(None);
47        }
48        let column = self.next_column;
49        self.next_column += 1;
50        read_column(&self.source, column).await.map(Some)
51    }
52}
53
54fn dimension_extent(value: &Value) -> Result<usize, RuntimeError> {
55    match value {
56        Value::Num(value)
57            if value.is_finite()
58                && value.fract() == 0.0
59                && *value >= 0.0
60                && *value <= usize::MAX as f64 =>
61        {
62            Ok(*value as usize)
63        }
64        Value::Int(value) => value.try_to_usize().ok_or_else(invalid_dimension),
65        other => Err(crate::runtime_error::semantic_error(
66            "InvalidLoopDimension",
67            format!("size returned a non-scalar loop dimension: {other:?}"),
68        )),
69    }
70}
71
72fn invalid_dimension() -> RuntimeError {
73    crate::runtime_error::semantic_error(
74        "InvalidLoopDimension",
75        "size returned a loop dimension outside platform limits",
76    )
77}
78
79async fn read_column(source: &Value, column: usize) -> Result<Value, RuntimeError> {
80    match source {
81        Value::Tensor(value) => {
82            read_tensor_slice_from_plan(value, &column_plan(&value.shape, column)?)
83        }
84        Value::ComplexTensor(value) => {
85            read_complex_slice_from_plan(value, &column_plan(&value.shape, column)?)
86        }
87        Value::SparseTensor(value) => {
88            read_sparse_slice_from_plan(value, &column_plan(&value.shape(), column)?)
89        }
90        Value::GpuTensor(value) => {
91            read_gpu_slice_from_plan(value, &column_plan(&value.shape, column)?)
92        }
93        Value::StringArray(value) => {
94            gather_string_slice(value, &column_plan(&value.shape, column)?)
95        }
96        Value::LogicalArray(value) => gather_logical(value, &column_plan(&value.shape, column)?),
97        Value::CharArray(value) => gather_char(value, &column_plan(value.shape(), column)?),
98        Value::Cell(value) => gather_cell(value, &column_plan(&value.shape, column)?),
99        Value::ObjectArray(value) => {
100            gather_object_array(value, &column_plan(value.shape(), column)?)
101        }
102        Value::SymbolicArray(value) => gather_symbolic(value, &column_plan(&value.shape, column)?),
103        Value::Object(_) | Value::HandleObject(_) => {
104            let descriptor = ObjectIndexDescriptor::subsref_paren_from_slice(
105                source.clone(),
106                2,
107                1,
108                0,
109                &[Value::Num((column + 1) as f64)],
110            )?;
111            call_object_index_descriptor_method(descriptor).await
112        }
113        // MATLAB scalars have size 1x1 and bind as themselves.
114        value => Ok(value.clone()),
115    }
116}
117
118fn column_plan(shape: &[usize], column: usize) -> Result<IndexPlan, RuntimeError> {
119    build_index_plan(
120        &[SliceSelector::Colon, SliceSelector::Scalar(column + 1)],
121        2,
122        shape,
123    )
124}
125
126fn gather_logical(value: &LogicalArray, plan: &IndexPlan) -> Result<Value, RuntimeError> {
127    let data = gather(&value.data, plan, "logical")?;
128    if data.len() == 1 {
129        return Ok(Value::Bool(data[0] != 0));
130    }
131    LogicalArray::new(data, plan.output_shape.clone())
132        .map(Value::LogicalArray)
133        .map_err(shape_error)
134}
135
136fn gather_char(value: &CharArray, plan: &IndexPlan) -> Result<Value, RuntimeError> {
137    let data = gather(&value.to_column_major(), plan, "character")?;
138    CharArray::from_column_major(data, plan.output_shape.clone())
139        .map(Value::CharArray)
140        .map_err(shape_error)
141}
142
143fn gather_cell(value: &CellArray, plan: &IndexPlan) -> Result<Value, RuntimeError> {
144    let indices = plan
145        .indices
146        .iter()
147        .map(|index| *index as usize + 1)
148        .collect::<Vec<_>>();
149    crate::object::cell::gather_cell_paren_linear_indices(value, &indices, &plan.output_shape)
150}
151
152fn gather_object_array(value: &ObjectArray, plan: &IndexPlan) -> Result<Value, RuntimeError> {
153    if let [index] = plan.indices.as_slice() {
154        return value.get_linear(*index as usize).cloned().ok_or_else(|| {
155            crate::runtime_error::semantic_error("IndexOutOfBounds", "Index out of bounds")
156        });
157    }
158    let indices = plan
159        .indices
160        .iter()
161        .map(|index| *index as usize)
162        .collect::<Vec<_>>();
163    value
164        .select_linear(&indices, plan.output_shape.clone())
165        .map(Value::ObjectArray)
166        .map_err(shape_error)
167}
168
169fn gather_symbolic(value: &SymbolicArray, plan: &IndexPlan) -> Result<Value, RuntimeError> {
170    let data = gather(&value.data, plan, "symbolic")?;
171    SymbolicArray::new(data, plan.output_shape.clone())
172        .map(Value::SymbolicArray)
173        .map_err(shape_error)
174}
175
176fn gather<T: Clone>(data: &[T], plan: &IndexPlan, kind: &str) -> Result<Vec<T>, RuntimeError> {
177    plan.indices
178        .iter()
179        .map(|index| {
180            data.get(*index as usize).cloned().ok_or_else(|| {
181                crate::runtime_error::semantic_error(
182                    "IndexOutOfBounds",
183                    format!("{kind} loop column index is out of bounds"),
184                )
185            })
186        })
187        .collect()
188}
189
190fn shape_error(error: impl std::fmt::Display) -> RuntimeError {
191    crate::runtime_error::semantic_error("ShapeMismatch", error.to_string())
192}
193
194#[cfg(test)]
195mod tests {
196    use std::rc::Rc;
197
198    use futures::executor::block_on;
199    use runmat_value::{CellArray, IntValue, IntegerStorage, Tensor, Value};
200
201    use super::ForColumnIterator;
202    use crate::context::RuntimeContext;
203    use crate::execution::RuntimeExecutionService;
204
205    fn collect(source: Value) -> Vec<Value> {
206        let runtime = RuntimeContext::new(Rc::new(RuntimeExecutionService::new()));
207        block_on(runtime.scope(async move {
208            let mut columns = ForColumnIterator::new(source).await.unwrap();
209            let mut values = Vec::new();
210            while let Some(value) = columns.next().await.unwrap() {
211                values.push(value);
212            }
213            values
214        }))
215    }
216
217    #[test]
218    fn iterates_dense_columns_without_losing_integer_storage() {
219        let tensor = Tensor::new_integer(
220            IntegerStorage::U64(vec![1_u64 << 63, u64::MAX, 7, 8]),
221            vec![2, 2],
222        )
223        .unwrap();
224        let columns = collect(Value::Tensor(tensor));
225        assert_eq!(columns.len(), 2);
226        let Value::Tensor(first) = &columns[0] else {
227            panic!("expected a tensor column")
228        };
229        assert_eq!(
230            first.integer_storage(),
231            Some(&IntegerStorage::U64(vec![1_u64 << 63, u64::MAX]))
232        );
233    }
234
235    #[test]
236    fn iterates_cell_columns_with_matlab_paren_shape() {
237        let cell = CellArray::from_column_major(
238            vec![
239                Value::Int(IntValue::I32(1)),
240                Value::Int(IntValue::I32(2)),
241                Value::Int(IntValue::I32(3)),
242                Value::Int(IntValue::I32(4)),
243            ],
244            vec![2, 2],
245        )
246        .unwrap();
247        let columns = collect(Value::Cell(cell));
248        let Value::Cell(second) = &columns[1] else {
249            panic!("expected a cell column")
250        };
251        assert_eq!(second.shape, vec![2, 1]);
252        assert_eq!(
253            second.to_column_major(),
254            vec![Value::Int(IntValue::I32(3)), Value::Int(IntValue::I32(4))]
255        );
256    }
257}