Skip to main content

runmat_runtime/call/
arguments.rs

1use serde::{Deserialize, Serialize};
2
3use crate::object::dispatch::call_object_index_descriptor_method_with_outputs;
4use crate::object::indexing::{ObjectIndexDescriptor, ObjectIndexSelector};
5use crate::{runtime_error::semantic_error, RuntimeError};
6use runmat_value::Value;
7
8/// Describes one source-level call argument after lowering.
9///
10/// The descriptor is shared by bytecode and native execution. It captures only
11/// language-level expansion semantics and deliberately contains no operand-stack
12/// or instruction-decoding state.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ArgumentSpec {
15    pub is_expand: bool,
16    pub num_indices: usize,
17    pub expand_all: bool,
18}
19
20/// One source-level argument after an executor has materialized its operands.
21///
22/// VM stack order and Native IR value identities stay in their executors; the
23/// expansion rules themselves are shared Runtime language semantics.
24#[derive(Debug, Clone)]
25pub enum MaterializedArgument {
26    Single(Value),
27    Expansion {
28        base: Value,
29        indices: Vec<Value>,
30        expand_all: bool,
31    },
32}
33
34pub async fn expand_arguments(
35    arguments: Vec<MaterializedArgument>,
36) -> Result<Vec<Value>, RuntimeError> {
37    let mut expanded_arguments = Vec::new();
38    for argument in arguments {
39        match argument {
40            MaterializedArgument::Single(value) => expanded_arguments.push(value),
41            MaterializedArgument::Expansion {
42                base,
43                indices,
44                expand_all,
45            } => {
46                let values = if expand_all {
47                    match base {
48                        Value::OutputList(outputs) => outputs,
49                        Value::Cell(cell) => crate::object::cell::expand_all_cell_values(&cell)?,
50                        base @ (Value::Object(_) | Value::HandleObject(_)) => {
51                            expand_brace_values(base, &[], None).await?
52                        }
53                        _ => {
54                            return Err(semantic_error(
55                                "InvalidExpandAllTarget",
56                                "Comma-separated-list expansion requires a cell array, output list, or object",
57                            ));
58                        }
59                    }
60                } else {
61                    match (base, indices.len()) {
62                        (Value::Cell(cell), 1 | 2) => {
63                            crate::object::cell::expand_cell_indices(&cell, &indices)?
64                        }
65                        (Value::OutputList(outputs), 1 | 2) => {
66                            let cols = outputs.len();
67                            let cell = runmat_value::CellArray::new(outputs, 1, cols).map_err(
68                                |error| {
69                                    semantic_error(
70                                        "ShapeMismatch",
71                                        format!("output-list expansion: {error}"),
72                                    )
73                                },
74                            )?;
75                            crate::object::cell::expand_cell_indices(&cell, &indices)?
76                        }
77                        (base @ (Value::Object(_) | Value::HandleObject(_)), _) => {
78                            expand_brace_values(base, &indices, None).await?
79                        }
80                        _ => {
81                            return Err(semantic_error(
82                                "InvalidExpandTarget",
83                                "Indexed comma-separated-list expansion requires a cell array, output list, or object",
84                            ));
85                        }
86                    }
87                };
88                expanded_arguments.extend(values);
89            }
90        }
91    }
92    Ok(expanded_arguments)
93}
94
95pub async fn expand_brace_values(
96    base: Value,
97    indices: &[Value],
98    pad_to_outputs: Option<usize>,
99) -> Result<Vec<Value>, RuntimeError> {
100    let mut values = match base {
101        Value::Cell(cell) => {
102            if indices.is_empty() {
103                if let Some(output_count) = pad_to_outputs {
104                    crate::object::cell::expand_cell_values(&cell, &[], output_count)?
105                } else {
106                    crate::object::cell::expand_all_cell_values(&cell)?
107                }
108            } else {
109                crate::object::cell::expand_cell_indices(&cell, indices)?
110            }
111        }
112        base @ (Value::Object(_) | Value::HandleObject(_)) => {
113            let value = call_object_index_descriptor_method_with_outputs(
114                ObjectIndexDescriptor::subsref_brace(
115                    base,
116                    ObjectIndexSelector::IndexValues {
117                        values: indices.to_vec(),
118                    },
119                ),
120                pad_to_outputs.unwrap_or(1),
121            )
122            .await?;
123            match value {
124                Value::OutputList(values) => values,
125                value => vec![value],
126            }
127        }
128        _ => {
129            return Err(semantic_error(
130                "CellExpansionOnNonCell",
131                "Cell expansion on non-cell",
132            ));
133        }
134    };
135    if let Some(output_count) = pad_to_outputs {
136        if values.len() > output_count {
137            values.truncate(output_count);
138        } else {
139            values.resize(output_count, Value::Num(0.0));
140        }
141    }
142    Ok(values)
143}
144
145#[cfg(test)]
146mod tests {
147    use futures::executor::block_on;
148    use runmat_value::{CellArray, Tensor, Value};
149
150    use super::{expand_arguments, MaterializedArgument};
151
152    #[test]
153    fn expansion_preserves_source_and_comma_list_order() {
154        let cell = CellArray::new(vec![Value::Num(2.0), Value::Num(3.0)], 1, 2).unwrap();
155        let values = block_on(expand_arguments(vec![
156            MaterializedArgument::Single(Value::Num(1.0)),
157            MaterializedArgument::Expansion {
158                base: Value::Cell(cell),
159                indices: Vec::new(),
160                expand_all: true,
161            },
162            MaterializedArgument::Single(Value::Num(4.0)),
163        ]))
164        .expect("expand arguments");
165        assert_eq!(
166            values,
167            vec![
168                Value::Num(1.0),
169                Value::Num(2.0),
170                Value::Num(3.0),
171                Value::Num(4.0),
172            ]
173        );
174    }
175
176    #[test]
177    fn output_list_index_expansion_uses_cell_index_semantics() {
178        let values = block_on(expand_arguments(vec![MaterializedArgument::Expansion {
179            base: Value::OutputList(vec![Value::Num(9.0), Value::Num(2.0)]),
180            indices: vec![Value::Tensor(
181                Tensor::new(vec![1.0, 2.0], vec![1, 2]).unwrap(),
182            )],
183            expand_all: false,
184        }]))
185        .expect("expand output list");
186        assert_eq!(values, vec![Value::Num(9.0), Value::Num(2.0)]);
187    }
188
189    #[test]
190    fn invalid_expansion_retains_stable_identifier() {
191        let error = block_on(expand_arguments(vec![MaterializedArgument::Expansion {
192            base: Value::Num(1.0),
193            indices: Vec::new(),
194            expand_all: true,
195        }]))
196        .expect_err("numeric expansion must fail");
197        assert_eq!(error.identifier(), Some("RunMat:InvalidExpandAllTarget"));
198    }
199}