formualizer_eval/builtins/lookup/
stack.rs

1//! Stack / concatenation dynamic array functions: HSTACK, VSTACK
2//!
3//! Excel semantics (baseline subset):
4//! - Each function accepts 1..N arrays/ranges; scalars treated as 1x1.
5//! - HSTACK: concatenate arrays horizontally (columns) aligning rows; differing row counts -> #VALUE!.
6//! - VSTACK: concatenate arrays vertically (rows) aligning columns; differing column counts -> #VALUE!.
7//! - Empty arguments (zero-sized ranges) are skipped; if all skipped -> empty spill.
8//! - Result collapses to scalar if 1x1 after stacking (consistent with existing dynamic functions here).
9//!
10//! TODO(excel-nuance): Propagate first error cell wise; currently a whole argument that is an Error scalar becomes a 1x1 error block.
11//! TODO(perf): Avoid intermediate full materialization by streaming row-wise/col-wise (later optimization).
12
13use crate::args::{ArgSchema, CoercionPolicy, ShapeKind};
14use crate::function::Function;
15use crate::traits::{ArgumentHandle, FunctionContext};
16use formualizer_common::{ArgKind, ExcelError, ExcelErrorKind, LiteralValue};
17use formualizer_macros::func_caps;
18
19#[derive(Debug)]
20pub struct HStackFn;
21#[derive(Debug)]
22pub struct VStackFn;
23
24fn materialize_arg(
25    arg: &ArgumentHandle,
26    ctx: &dyn FunctionContext,
27) -> Result<Vec<Vec<LiteralValue>>, ExcelError> {
28    // Similar helper to dynamic.rs (avoid cyclic import). Minimal duplication; consider refactor later.
29    if let Ok(r) = arg.as_reference_or_eval() {
30        let mut rows: Vec<Vec<LiteralValue>> = Vec::new();
31        let sheet = "Sheet1"; // TODO propagate sheet
32        let rv = ctx.resolve_range_view(&r, sheet)?;
33        rv.for_each_row(&mut |row| {
34            rows.push(row.to_vec());
35            Ok(())
36        })?;
37        Ok(rows)
38    } else {
39        match arg.value()?.as_ref() {
40            LiteralValue::Array(a) => Ok(a.clone()),
41            v => Ok(vec![vec![v.clone()]]),
42        }
43    }
44}
45
46fn collapse_if_scalar(mut rows: Vec<Vec<LiteralValue>>) -> LiteralValue {
47    if rows.len() == 1 && rows[0].len() == 1 {
48        return rows.remove(0).remove(0);
49    }
50    LiteralValue::Array(rows)
51}
52
53impl Function for HStackFn {
54    func_caps!(PURE);
55    fn name(&self) -> &'static str {
56        "HSTACK"
57    }
58    fn min_args(&self) -> usize {
59        1
60    }
61    fn variadic(&self) -> bool {
62        true
63    }
64    fn arg_schema(&self) -> &'static [ArgSchema] {
65        use once_cell::sync::Lazy;
66        static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(|| {
67            vec![ArgSchema {
68                kinds: smallvec::smallvec![ArgKind::Range, ArgKind::Any],
69                required: true,
70                by_ref: false,
71                shape: ShapeKind::Range,
72                coercion: CoercionPolicy::None,
73                max: None,
74                repeating: Some(1),
75                default: None,
76            }]
77        });
78        &SCHEMA
79    }
80    fn eval_scalar<'a, 'b>(
81        &self,
82        args: &'a [ArgumentHandle<'a, 'b>],
83        ctx: &dyn FunctionContext,
84    ) -> Result<LiteralValue, ExcelError> {
85        if args.is_empty() {
86            return Ok(LiteralValue::Array(vec![]));
87        }
88        let mut blocks: Vec<Vec<Vec<LiteralValue>>> = Vec::new();
89        let mut target_rows: Option<usize> = None;
90        for a in args {
91            let rows = materialize_arg(a, ctx)?;
92            if rows.is_empty() {
93                continue;
94            }
95            if let Some(tr) = target_rows {
96                if rows.len() != tr {
97                    return Ok(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value)));
98                }
99            } else {
100                target_rows = Some(rows.len());
101            }
102            blocks.push(rows);
103        }
104        if blocks.is_empty() {
105            return Ok(LiteralValue::Array(vec![]));
106        }
107        let row_count = target_rows.unwrap();
108        // Compute total width (use first row lengths; mismatched row internal widths cause #VALUE!)
109        for b in &blocks {
110            // rectangular validation inside block
111            let w = b[0].len();
112            if b.iter().any(|r| r.len() != w) {
113                return Ok(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value)));
114            }
115        }
116        let mut result: Vec<Vec<LiteralValue>> = Vec::with_capacity(row_count);
117        for r in 0..row_count {
118            result.push(Vec::new());
119        }
120        for b in blocks {
121            for (r, row_vec) in b.into_iter().enumerate() {
122                result[r].extend(row_vec);
123            }
124        }
125        Ok(collapse_if_scalar(result))
126    }
127}
128
129impl Function for VStackFn {
130    func_caps!(PURE);
131    fn name(&self) -> &'static str {
132        "VSTACK"
133    }
134    fn min_args(&self) -> usize {
135        1
136    }
137    fn variadic(&self) -> bool {
138        true
139    }
140    fn arg_schema(&self) -> &'static [ArgSchema] {
141        use once_cell::sync::Lazy;
142        static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(|| {
143            vec![ArgSchema {
144                kinds: smallvec::smallvec![ArgKind::Range, ArgKind::Any],
145                required: true,
146                by_ref: false,
147                shape: ShapeKind::Range,
148                coercion: CoercionPolicy::None,
149                max: None,
150                repeating: Some(1),
151                default: None,
152            }]
153        });
154        &SCHEMA
155    }
156    fn eval_scalar<'a, 'b>(
157        &self,
158        args: &'a [ArgumentHandle<'a, 'b>],
159        ctx: &dyn FunctionContext,
160    ) -> Result<LiteralValue, ExcelError> {
161        if args.is_empty() {
162            return Ok(LiteralValue::Array(vec![]));
163        }
164        let mut blocks: Vec<Vec<Vec<LiteralValue>>> = Vec::new();
165        let mut target_width: Option<usize> = None;
166        for a in args {
167            let rows = materialize_arg(a, ctx)?;
168            if rows.is_empty() {
169                continue;
170            }
171            // Determine width (validate rectangular within block)
172            let width = rows[0].len();
173            if rows.iter().any(|r| r.len() != width) {
174                return Ok(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value)));
175            }
176            if let Some(tw) = target_width {
177                if width != tw {
178                    return Ok(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Value)));
179                }
180            } else {
181                target_width = Some(width);
182            }
183            blocks.push(rows);
184        }
185        if blocks.is_empty() {
186            return Ok(LiteralValue::Array(vec![]));
187        }
188        let mut result: Vec<Vec<LiteralValue>> = Vec::new();
189        for b in blocks {
190            result.extend(b);
191        }
192        Ok(collapse_if_scalar(result))
193    }
194}
195
196pub fn register_builtins() {
197    use crate::function_registry::register_function;
198    use std::sync::Arc;
199    register_function(Arc::new(HStackFn));
200    register_function(Arc::new(VStackFn));
201}
202
203/* ───────────────────────── tests ───────────────────────── */
204#[cfg(test)]
205mod tests {
206    use super::*;
207    use crate::test_workbook::TestWorkbook;
208    use crate::traits::ArgumentHandle;
209    use formualizer_parse::parser::{ASTNode, ASTNodeType, ReferenceType};
210    use std::sync::Arc;
211
212    fn ref_range(r: &str, sr: i32, sc: i32, er: i32, ec: i32) -> ASTNode {
213        ASTNode::new(
214            ASTNodeType::Reference {
215                original: r.into(),
216                reference: ReferenceType::Range {
217                    sheet: None,
218                    start_row: Some(sr as u32),
219                    start_col: Some(sc as u32),
220                    end_row: Some(er as u32),
221                    end_col: Some(ec as u32),
222                },
223            },
224            None,
225        )
226    }
227
228    fn lit(v: LiteralValue) -> ASTNode {
229        ASTNode::new(ASTNodeType::Literal(v), None)
230    }
231
232    #[test]
233    fn hstack_basic_and_mismatched_rows() {
234        let wb = TestWorkbook::new().with_function(Arc::new(HStackFn));
235        let wb = wb
236            .with_cell_a1("Sheet1", "A1", LiteralValue::Int(1))
237            .with_cell_a1("Sheet1", "A2", LiteralValue::Int(2))
238            .with_cell_a1("Sheet1", "B1", LiteralValue::Int(10))
239            .with_cell_a1("Sheet1", "B2", LiteralValue::Int(20))
240            .with_cell_a1("Sheet1", "C1", LiteralValue::Int(100)); // single row range for mismatch
241        let ctx = wb.interpreter();
242        let left = ref_range("A1:A2", 1, 1, 2, 1);
243        let right = ref_range("B1:B2", 1, 2, 2, 2);
244        let f = ctx.context.get_function("", "HSTACK").unwrap();
245        let args = vec![
246            ArgumentHandle::new(&left, &ctx),
247            ArgumentHandle::new(&right, &ctx),
248        ];
249        let v = f.dispatch(&args, &ctx.function_context(None)).unwrap();
250        match v {
251            LiteralValue::Array(a) => {
252                assert_eq!(a.len(), 2);
253                assert_eq!(a[0], vec![LiteralValue::Int(1), LiteralValue::Int(10)]);
254            }
255            other => panic!("expected array got {other:?}"),
256        }
257        // mismatch rows
258        let mism = ref_range("C1:C1", 1, 3, 1, 3);
259        let args_bad = vec![
260            ArgumentHandle::new(&left, &ctx),
261            ArgumentHandle::new(&mism, &ctx),
262        ];
263        let v_bad = f.dispatch(&args_bad, &ctx.function_context(None)).unwrap();
264        match v_bad {
265            LiteralValue::Error(e) => assert_eq!(e.kind, ExcelErrorKind::Value),
266            other => panic!("expected #VALUE! got {other:?}"),
267        }
268    }
269
270    #[test]
271    fn vstack_basic_and_mismatched_cols() {
272        let wb = TestWorkbook::new().with_function(Arc::new(VStackFn));
273        let wb = wb
274            .with_cell_a1("Sheet1", "A1", LiteralValue::Int(1))
275            .with_cell_a1("Sheet1", "B1", LiteralValue::Int(10))
276            .with_cell_a1("Sheet1", "A2", LiteralValue::Int(2))
277            .with_cell_a1("Sheet1", "B2", LiteralValue::Int(20))
278            .with_cell_a1("Sheet1", "C1", LiteralValue::Int(100))
279            .with_cell_a1("Sheet1", "C2", LiteralValue::Int(200));
280        let ctx = wb.interpreter();
281        let top = ref_range("A1:B1", 1, 1, 1, 2);
282        let bottom = ref_range("A2:B2", 2, 1, 2, 2);
283        let f = ctx.context.get_function("", "VSTACK").unwrap();
284        let args = vec![
285            ArgumentHandle::new(&top, &ctx),
286            ArgumentHandle::new(&bottom, &ctx),
287        ];
288        let v = f.dispatch(&args, &ctx.function_context(None)).unwrap();
289        match v {
290            LiteralValue::Array(a) => {
291                assert_eq!(a.len(), 2);
292                assert_eq!(a[0], vec![LiteralValue::Int(1), LiteralValue::Int(10)]);
293            }
294            other => panic!("expected array got {other:?}"),
295        }
296        // mismatched width (add 3rd column row)
297        let extra = ref_range("A1:C1", 1, 1, 1, 3);
298        let args_bad = vec![
299            ArgumentHandle::new(&top, &ctx),
300            ArgumentHandle::new(&extra, &ctx),
301        ];
302        let v_bad = f.dispatch(&args_bad, &ctx.function_context(None)).unwrap();
303        match v_bad {
304            LiteralValue::Error(e) => assert_eq!(e.kind, ExcelErrorKind::Value),
305            other => panic!("expected #VALUE! got {other:?}"),
306        }
307    }
308
309    #[test]
310    fn hstack_scalar_and_array_collapse() {
311        let wb = TestWorkbook::new().with_function(Arc::new(HStackFn));
312        let ctx = wb.interpreter();
313        let f = ctx.context.get_function("", "HSTACK").unwrap();
314        let s1 = lit(LiteralValue::Int(5));
315        let s2 = lit(LiteralValue::Int(6));
316        let args = vec![
317            ArgumentHandle::new(&s1, &ctx),
318            ArgumentHandle::new(&s2, &ctx),
319        ];
320        let v = f.dispatch(&args, &ctx.function_context(None)).unwrap();
321        // 1 row x 2 cols stays as array (not scalar collapse)
322        match v {
323            LiteralValue::Array(a) => {
324                assert_eq!(a.len(), 1);
325                assert_eq!(a[0], vec![LiteralValue::Int(5), LiteralValue::Int(6)]);
326            }
327            other => panic!("expected array got {other:?}"),
328        }
329    }
330
331    #[test]
332    fn vstack_scalar_collapse_single_result() {
333        let wb = TestWorkbook::new().with_function(Arc::new(VStackFn));
334        let ctx = wb.interpreter();
335        let f = ctx.context.get_function("", "VSTACK").unwrap();
336        let lone = lit(LiteralValue::Int(9));
337        let args = vec![ArgumentHandle::new(&lone, &ctx)];
338        let v = f.dispatch(&args, &ctx.function_context(None)).unwrap();
339        assert_eq!(v, LiteralValue::Int(9));
340    }
341}