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#[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#[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}