formualizer_eval/builtins/
reference_fns.rs

1use crate::args::{ArgSchema, CoercionPolicy, ShapeKind};
2use crate::function::{FnCaps, Function};
3use crate::traits::{ArgumentHandle, FunctionContext};
4use formualizer_common::{ArgKind, ExcelError, ExcelErrorKind, LiteralValue};
5use formualizer_parse::parser::ReferenceType;
6
7fn number_strict_scalar() -> ArgSchema {
8    ArgSchema {
9        kinds: smallvec::smallvec![ArgKind::Number],
10        required: true,
11        by_ref: false,
12        shape: ShapeKind::Scalar,
13        coercion: CoercionPolicy::NumberStrict,
14        max: None,
15        repeating: None,
16        default: None,
17    }
18}
19
20fn arg_byref_array() -> Vec<ArgSchema> {
21    vec![
22        ArgSchema {
23            kinds: smallvec::smallvec![ArgKind::Range],
24            required: true,
25            by_ref: true,
26            shape: ShapeKind::Range,
27            coercion: CoercionPolicy::None,
28            max: None,
29            repeating: None,
30            default: None,
31        },
32        number_strict_scalar(),
33        number_strict_scalar(),
34    ]
35}
36
37fn arg_byref_reference() -> Vec<ArgSchema> {
38    vec![
39        ArgSchema {
40            kinds: smallvec::smallvec![ArgKind::Range],
41            required: true,
42            by_ref: true,
43            shape: ShapeKind::Range,
44            coercion: CoercionPolicy::None,
45            max: None,
46            repeating: None,
47            default: None,
48        },
49        number_strict_scalar(),
50        number_strict_scalar(),
51        ArgSchema {
52            // height optional
53            kinds: smallvec::smallvec![ArgKind::Number],
54            required: false,
55            by_ref: false,
56            shape: ShapeKind::Scalar,
57            coercion: CoercionPolicy::NumberStrict,
58            max: None,
59            repeating: None,
60            default: None,
61        },
62        ArgSchema {
63            // width optional
64            kinds: smallvec::smallvec![ArgKind::Number],
65            required: false,
66            by_ref: false,
67            shape: ShapeKind::Scalar,
68            coercion: CoercionPolicy::NumberStrict,
69            max: None,
70            repeating: None,
71            default: None,
72        },
73    ]
74}
75
76#[derive(Debug)]
77pub struct IndexFn;
78impl Function for IndexFn {
79    fn caps(&self) -> FnCaps {
80        FnCaps::PURE | FnCaps::RETURNS_REFERENCE
81    }
82    fn name(&self) -> &'static str {
83        "INDEX"
84    }
85    fn min_args(&self) -> usize {
86        3
87    }
88    fn arg_schema(&self) -> &'static [ArgSchema] {
89        use once_cell::sync::Lazy;
90        static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(arg_byref_array);
91        &SCHEMA
92    }
93
94    fn eval_reference<'a, 'b>(
95        &self,
96        args: &'a [ArgumentHandle<'a, 'b>],
97        _ctx: &dyn FunctionContext,
98    ) -> Option<Result<ReferenceType, ExcelError>> {
99        // args: array(by_ref), row, col
100        if args.len() < 3 {
101            return Some(Err(ExcelError::new(ExcelErrorKind::Value)));
102        }
103        let base = match args[0].as_reference_or_eval() {
104            Ok(r) => r,
105            Err(e) => return Some(Err(e)),
106        };
107        let row = match args[1].value() {
108            Ok(v) => match v.as_ref() {
109                LiteralValue::Number(n) => *n as i64,
110                LiteralValue::Int(i) => *i,
111                _ => return Some(Err(ExcelError::new(ExcelErrorKind::Value))),
112            },
113            Err(e) => return Some(Err(e)),
114        };
115        let col = match args[2].value() {
116            Ok(v) => match v.as_ref() {
117                LiteralValue::Number(n) => *n as i64,
118                LiteralValue::Int(i) => *i,
119                _ => return Some(Err(ExcelError::new(ExcelErrorKind::Value))),
120            },
121            Err(e) => return Some(Err(e)),
122        };
123
124        // Only Range supported for now
125        let (sheet, sr, sc, er, ec) = match base {
126            ReferenceType::Range {
127                sheet,
128                start_row,
129                start_col,
130                end_row,
131                end_col,
132            } => match (start_row, start_col, end_row, end_col) {
133                (Some(sr), Some(sc), Some(er), Some(ec)) => (sheet, sr, sc, er, ec),
134                _ => return Some(Err(ExcelError::new(ExcelErrorKind::Ref))),
135            },
136            ReferenceType::Cell { sheet, row, col } => (sheet, row, col, row, col),
137            _ => return Some(Err(ExcelError::new(ExcelErrorKind::Ref))),
138        };
139
140        // 1-based indexing per Excel
141        if row <= 0 || col <= 0 {
142            return Some(Err(ExcelError::new(ExcelErrorKind::Ref)));
143        }
144        let r = sr + (row as u32) - 1;
145        let c = sc + (col as u32) - 1;
146        if r > er || c > ec {
147            return Some(Err(ExcelError::new(ExcelErrorKind::Ref)));
148        }
149
150        Some(Ok(ReferenceType::Cell {
151            sheet,
152            row: r,
153            col: c,
154        }))
155    }
156
157    fn eval_scalar<'a, 'b>(
158        &self,
159        args: &'a [ArgumentHandle<'a, 'b>],
160        ctx: &dyn FunctionContext,
161    ) -> Result<LiteralValue, ExcelError> {
162        if let Some(Ok(r)) = self.eval_reference(args, ctx) {
163            // Materialize to value
164            let current_sheet = ctx.current_sheet();
165            match ctx.resolve_range_view(&r, current_sheet) {
166                Ok(rv) => {
167                    let (rows, cols) = rv.dims();
168                    if rows == 1 && cols == 1 {
169                        Ok(rv.as_1x1().unwrap_or(LiteralValue::Empty))
170                    } else {
171                        let mut rows_out: Vec<Vec<LiteralValue>> = Vec::new();
172                        rv.for_each_row(&mut |row| {
173                            rows_out.push(row.to_vec());
174                            Ok(())
175                        })?;
176                        Ok(LiteralValue::Array(rows_out))
177                    }
178                }
179                Err(e) => Ok(LiteralValue::Error(e)),
180            }
181        } else {
182            Ok(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Ref)))
183        }
184    }
185}
186
187#[derive(Debug)]
188pub struct OffsetFn;
189impl Function for OffsetFn {
190    fn caps(&self) -> FnCaps {
191        // OFFSET is volatile in Excel semantics
192        FnCaps::PURE | FnCaps::RETURNS_REFERENCE | FnCaps::VOLATILE
193    }
194    fn name(&self) -> &'static str {
195        "OFFSET"
196    }
197    fn min_args(&self) -> usize {
198        3
199    }
200    fn arg_schema(&self) -> &'static [ArgSchema] {
201        use once_cell::sync::Lazy;
202        static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(arg_byref_reference);
203        &SCHEMA
204    }
205
206    fn eval_reference<'a, 'b>(
207        &self,
208        args: &'a [ArgumentHandle<'a, 'b>],
209        _ctx: &dyn FunctionContext,
210    ) -> Option<Result<ReferenceType, ExcelError>> {
211        if args.len() < 3 {
212            return Some(Err(ExcelError::new(ExcelErrorKind::Value)));
213        }
214        let base = match args[0].as_reference_or_eval() {
215            Ok(r) => r,
216            Err(e) => return Some(Err(e)),
217        };
218        let dr = match args[1].value() {
219            Ok(v) => match v.as_ref() {
220                LiteralValue::Number(n) => *n as i64,
221                LiteralValue::Int(i) => *i,
222                _ => return Some(Err(ExcelError::new(ExcelErrorKind::Value))),
223            },
224            Err(e) => return Some(Err(e)),
225        };
226        let dc = match args[2].value() {
227            Ok(v) => match v.as_ref() {
228                LiteralValue::Number(n) => *n as i64,
229                LiteralValue::Int(i) => *i,
230                _ => return Some(Err(ExcelError::new(ExcelErrorKind::Value))),
231            },
232            Err(e) => return Some(Err(e)),
233        };
234
235        let (sheet, sr, sc, er, ec) = match base {
236            ReferenceType::Range {
237                sheet,
238                start_row,
239                start_col,
240                end_row,
241                end_col,
242            } => match (start_row, start_col, end_row, end_col) {
243                (Some(sr), Some(sc), Some(er), Some(ec)) => (sheet, sr, sc, er, ec),
244                _ => return Some(Err(ExcelError::new(ExcelErrorKind::Ref))),
245            },
246            ReferenceType::Cell { sheet, row, col } => (sheet, row, col, row, col),
247            _ => return Some(Err(ExcelError::new(ExcelErrorKind::Ref))),
248        };
249
250        let nsr = (sr as i64) + dr;
251        let nsc = (sc as i64) + dc;
252        let height = if args.len() >= 4 {
253            match args[3].value() {
254                Ok(v) => match v.as_ref() {
255                    LiteralValue::Number(n) => *n as i64,
256                    LiteralValue::Int(i) => *i,
257                    _ => return Some(Err(ExcelError::new(ExcelErrorKind::Value))),
258                },
259                Err(e) => return Some(Err(e)),
260            }
261        } else {
262            (er as i64) - (sr as i64) + 1
263        };
264        let width = if args.len() >= 5 {
265            match args[4].value() {
266                Ok(v) => match v.as_ref() {
267                    LiteralValue::Number(n) => *n as i64,
268                    LiteralValue::Int(i) => *i,
269                    _ => return Some(Err(ExcelError::new(ExcelErrorKind::Value))),
270                },
271                Err(e) => return Some(Err(e)),
272            }
273        } else {
274            (ec as i64) - (sc as i64) + 1
275        };
276
277        if nsr <= 0 || nsc <= 0 || height <= 0 || width <= 0 {
278            return Some(Err(ExcelError::new(ExcelErrorKind::Ref)));
279        }
280        let ner = nsr + height - 1;
281        let nec = nsc + width - 1;
282
283        if height == 1 && width == 1 {
284            Some(Ok(ReferenceType::Cell {
285                sheet,
286                row: nsr as u32,
287                col: nsc as u32,
288            }))
289        } else {
290            Some(Ok(ReferenceType::Range {
291                sheet,
292                start_row: Some(nsr as u32),
293                start_col: Some(nsc as u32),
294                end_row: Some(ner as u32),
295                end_col: Some(nec as u32),
296            }))
297        }
298    }
299
300    fn eval_scalar<'a, 'b>(
301        &self,
302        args: &'a [ArgumentHandle<'a, 'b>],
303        ctx: &dyn FunctionContext,
304    ) -> Result<LiteralValue, ExcelError> {
305        if let Some(Ok(r)) = self.eval_reference(args, ctx) {
306            let current_sheet = ctx.current_sheet();
307            match ctx.resolve_range_view(&r, current_sheet) {
308                Ok(rv) => {
309                    let (rows, cols) = rv.dims();
310                    if rows == 1 && cols == 1 {
311                        Ok(rv.as_1x1().unwrap_or(LiteralValue::Empty))
312                    } else {
313                        let mut rows_out: Vec<Vec<LiteralValue>> = Vec::new();
314                        rv.for_each_row(&mut |row| {
315                            rows_out.push(row.to_vec());
316                            Ok(())
317                        })?;
318                        Ok(LiteralValue::Array(rows_out))
319                    }
320                }
321                Err(e) => Ok(LiteralValue::Error(e)),
322            }
323        } else {
324            Ok(LiteralValue::Error(ExcelError::new(ExcelErrorKind::Ref)))
325        }
326    }
327}
328
329pub fn register_builtins() {
330    crate::function_registry::register_function(std::sync::Arc::new(IndexFn));
331    crate::function_registry::register_function(std::sync::Arc::new(OffsetFn));
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337    use crate::test_workbook::TestWorkbook;
338    use crate::traits::ArgumentHandle;
339    use formualizer_parse::parser::{ASTNode, ASTNodeType};
340
341    fn interp(wb: &TestWorkbook) -> crate::interpreter::Interpreter<'_> {
342        wb.interpreter()
343    }
344
345    #[test]
346    fn index_returns_reference_and_materializes_in_value_context() {
347        let wb = TestWorkbook::new()
348            .with_cell_a1("Sheet1", "B2", LiteralValue::Int(42))
349            .with_function(std::sync::Arc::new(IndexFn));
350        let ctx = interp(&wb);
351
352        // Build INDEX(A1:C3,2,2) expecting B2
353        let array_ref = ASTNode::new(
354            ASTNodeType::Reference {
355                original: "A1:C3".into(),
356                reference: ReferenceType::Range {
357                    sheet: None,
358                    start_row: Some(1),
359                    start_col: Some(1),
360                    end_row: Some(3),
361                    end_col: Some(3),
362                },
363            },
364            None,
365        );
366        let row = ASTNode::new(ASTNodeType::Literal(LiteralValue::Int(2)), None);
367        let col = ASTNode::new(ASTNodeType::Literal(LiteralValue::Int(2)), None);
368        let call = ASTNode::new(
369            ASTNodeType::Function {
370                name: "INDEX".into(),
371                args: vec![array_ref.clone(), row.clone(), col.clone()],
372            },
373            None,
374        );
375
376        // Reference context
377        let r = ctx.evaluate_ast_as_reference(&call).expect("ref ok");
378        match r {
379            ReferenceType::Cell { row, col, .. } => {
380                assert_eq!((row, col), (2, 2));
381            }
382            _ => panic!(),
383        }
384
385        // Value context (scalar materialization)
386        let args = vec![
387            ArgumentHandle::new(&array_ref, &ctx),
388            ArgumentHandle::new(&row, &ctx),
389            ArgumentHandle::new(&col, &ctx),
390        ];
391        let f = ctx.context.get_function("", "INDEX").unwrap();
392        let v = f.dispatch(&args, &ctx.function_context(None)).unwrap();
393        assert_eq!(v, LiteralValue::Int(42));
394    }
395
396    #[test]
397    fn offset_returns_reference_and_materializes() {
398        let wb = TestWorkbook::new()
399            .with_cell_a1("Sheet1", "A1", LiteralValue::Int(1))
400            .with_cell_a1("Sheet1", "B2", LiteralValue::Int(5))
401            .with_function(std::sync::Arc::new(OffsetFn));
402        let ctx = interp(&wb);
403
404        let base = ASTNode::new(
405            ASTNodeType::Reference {
406                original: "A1".into(),
407                reference: ReferenceType::Cell {
408                    sheet: None,
409                    row: 1,
410                    col: 1,
411                },
412            },
413            None,
414        );
415        let dr = ASTNode::new(ASTNodeType::Literal(LiteralValue::Int(1)), None);
416        let dc = ASTNode::new(ASTNodeType::Literal(LiteralValue::Int(1)), None);
417        let call = ASTNode::new(
418            ASTNodeType::Function {
419                name: "OFFSET".into(),
420                args: vec![base.clone(), dr.clone(), dc.clone()],
421            },
422            None,
423        );
424
425        let r = ctx.evaluate_ast_as_reference(&call).expect("ref ok");
426        match r {
427            ReferenceType::Cell { row, col, .. } => assert_eq!((row, col), (2, 2)),
428            _ => panic!(),
429        }
430
431        let args = vec![
432            ArgumentHandle::new(&base, &ctx),
433            ArgumentHandle::new(&dr, &ctx),
434            ArgumentHandle::new(&dc, &ctx),
435        ];
436        let f = ctx.context.get_function("", "OFFSET").unwrap();
437        let v = f.dispatch(&args, &ctx.function_context(None)).unwrap();
438        assert_eq!(v, LiteralValue::Int(5));
439    }
440}