Skip to main content

formualizer_eval/builtins/
lambda.rs

1use crate::function::{FnCaps, Function};
2use crate::interpreter::{LocalBinding, LocalEnv};
3use crate::traits::{ArgumentHandle, CalcValue, CustomCallable, FunctionContext};
4use formualizer_common::{ExcelError, ExcelErrorKind, LiteralValue};
5use formualizer_parse::parser::{ASTNode, ASTNodeType, ReferenceType};
6use std::collections::HashSet;
7use std::sync::Arc;
8
9fn value_error(msg: impl Into<String>) -> ExcelError {
10    ExcelError::new(ExcelErrorKind::Value).with_message(msg.into())
11}
12
13fn local_name_from_ast(node: &ASTNode) -> Result<String, ExcelError> {
14    match &node.node_type {
15        ASTNodeType::Reference {
16            reference: ReferenceType::NamedRange(name),
17            ..
18        } => Ok(name.clone()),
19        _ => Err(value_error("Expected a local name identifier")),
20    }
21}
22
23fn binding_from_calc_value(cv: CalcValue<'_>) -> LocalBinding {
24    match cv {
25        CalcValue::Scalar(v) => LocalBinding::Value(v),
26        CalcValue::Range(rv) => {
27            let (rows, cols) = rv.dims();
28            if rows == 1 && cols == 1 {
29                LocalBinding::Value(rv.get_cell(0, 0))
30            } else {
31                let mut data = Vec::with_capacity(rows);
32                let _ = rv.for_each_row(&mut |row| {
33                    data.push(row.to_vec());
34                    Ok(())
35                });
36                LocalBinding::Value(LiteralValue::Array(data))
37            }
38        }
39        CalcValue::Callable(c) => LocalBinding::Callable(c),
40    }
41}
42
43#[derive(Debug)]
44pub struct LetFn;
45
46/// Binds local names to values and evaluates a final expression with those bindings.
47///
48/// `LET` introduces lexical variables using name/value pairs, then returns the last expression.
49///
50/// # Remarks
51/// - Arguments must be provided as `name, value` pairs followed by one final calculation expression.
52/// - Names are resolved as local identifiers and can shadow workbook-level names.
53/// - Bindings are evaluated left-to-right, so later values can reference earlier bindings.
54/// - Invalid names or malformed arity return `#VALUE!`.
55///
56/// # Examples
57///
58/// ```yaml,sandbox
59/// title: "Bind intermediate values"
60/// formula: "=LET(rate,0.08,price,125,price*(1+rate))"
61/// expected: 135
62/// ```
63///
64/// ```yaml,sandbox
65/// title: "Use LET with range calculations"
66/// grid:
67///   A1: 10
68///   A2: 4
69/// formula: "=LET(total,SUM(A1:A2),total*2)"
70/// expected: 28
71/// ```
72///
73/// ```yaml,sandbox
74/// title: "Nested LET supports shadowing"
75/// formula: "=LET(x,2,LET(x,5,x)+x)"
76/// expected: 7
77/// ```
78///
79/// ```yaml,docs
80/// related:
81///   - LAMBDA
82///   - IF
83///   - SUM
84///   - INDEX
85/// faq:
86///   - q: "Can a LET binding reference a name defined later in the same LET?"
87///     a: "No. LET evaluates name/value pairs left-to-right, so each binding can only use earlier bindings."
88///   - q: "Does LET overwrite workbook or worksheet names permanently?"
89///     a: "No. LET names are lexical and local to that formula evaluation; they only shadow outer names inside the LET expression."
90///   - q: "Is LET itself volatile?"
91///     a: "No. LET is deterministic unless one of its bound expressions calls a volatile function such as RAND."
92/// ```
93///
94/// [formualizer-docgen:schema:start]
95/// Name: LET
96/// Type: LetFn
97/// Min args: 3
98/// Max args: variadic
99/// Variadic: true
100/// Signature: LET(<schema unavailable>)
101/// Arg schema: <unavailable: arg_schema panicked>
102/// Caps: PURE, SHORT_CIRCUIT
103/// [formualizer-docgen:schema:end]
104impl Function for LetFn {
105    fn caps(&self) -> FnCaps {
106        FnCaps::PURE | FnCaps::SHORT_CIRCUIT
107    }
108
109    fn name(&self) -> &'static str {
110        "LET"
111    }
112
113    fn min_args(&self) -> usize {
114        3
115    }
116
117    fn variadic(&self) -> bool {
118        true
119    }
120
121    fn dispatch<'a, 'b, 'c>(
122        &self,
123        args: &'c [ArgumentHandle<'a, 'b>],
124        ctx: &dyn FunctionContext<'b>,
125    ) -> Result<CalcValue<'b>, ExcelError> {
126        self.eval(args, ctx)
127    }
128
129    fn eval<'a, 'b, 'c>(
130        &self,
131        args: &'c [ArgumentHandle<'a, 'b>],
132        _ctx: &dyn FunctionContext<'b>,
133    ) -> Result<CalcValue<'b>, ExcelError> {
134        if args.len() < 3 || args.len().is_multiple_of(2) {
135            return Ok(CalcValue::Scalar(LiteralValue::Error(value_error(
136                "LET expects name/value pairs followed by a final expression",
137            ))));
138        }
139
140        let mut env: LocalEnv = args[0].current_env();
141
142        for pair_idx in (0..args.len() - 1).step_by(2) {
143            let name = match local_name_from_ast(args[pair_idx].ast()) {
144                Ok(name) => name,
145                Err(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
146            };
147
148            let bound = args[pair_idx + 1].value_with_env(env.clone())?;
149            env = env.with_binding(&name, binding_from_calc_value(bound));
150        }
151
152        args[args.len() - 1].value_with_env(env)
153    }
154}
155
156#[derive(Clone)]
157struct LambdaClosure {
158    params: Vec<String>,
159    body: ASTNode,
160    captured_env: LocalEnv,
161}
162
163impl CustomCallable for LambdaClosure {
164    fn arity(&self) -> usize {
165        self.params.len()
166    }
167
168    fn invoke<'ctx>(
169        &self,
170        interp: &crate::interpreter::Interpreter<'ctx>,
171        args: &[LiteralValue],
172    ) -> Result<CalcValue<'ctx>, ExcelError> {
173        if args.len() != self.arity() {
174            return Ok(CalcValue::Scalar(LiteralValue::Error(value_error(
175                format!(
176                    "LAMBDA expected {} argument(s), got {}",
177                    self.arity(),
178                    args.len()
179                ),
180            ))));
181        }
182
183        let mut env = self.captured_env.clone();
184        for (name, value) in self.params.iter().zip(args.iter()) {
185            env = env.with_binding(name, LocalBinding::Value(value.clone()));
186        }
187
188        let scoped = interp.with_local_env(env);
189        scoped.evaluate_ast(&self.body)
190    }
191}
192
193#[derive(Debug)]
194pub struct LambdaFn;
195
196/// Creates an anonymous callable that can be invoked with spreadsheet arguments.
197///
198/// `LAMBDA` captures its defining local scope and returns a reusable function value.
199///
200/// # Remarks
201/// - All arguments except the last are parameter names; the last argument is the body expression.
202/// - Parameter names must be unique (case-insensitive), or `#VALUE!` is returned.
203/// - Invocation arity must exactly match the declared parameter count.
204/// - Returning an uninvoked lambda as a final cell value yields a `#CALC!` in evaluation.
205///
206/// # Examples
207///
208/// ```yaml,sandbox
209/// title: "Inline lambda invocation"
210/// formula: "=LAMBDA(x,x+1)(41)"
211/// expected: 42
212/// ```
213///
214/// ```yaml,sandbox
215/// title: "Lambda captures outer LET bindings"
216/// formula: "=LET(k,10,addk,LAMBDA(n,n+k),addk(5))"
217/// expected: 15
218/// ```
219///
220/// ```yaml,sandbox
221/// title: "Duplicate parameter names are invalid"
222/// formula: "=LAMBDA(x,x,x+1)"
223/// expected: "#VALUE!"
224/// ```
225///
226/// ```yaml,docs
227/// related:
228///   - LET
229///   - IF
230///   - SUM
231/// faq:
232///   - q: "Why does =LAMBDA(x,x+1) return #CALC! instead of a number?"
233///     a: "LAMBDA returns a callable value. In a cell result position, it must be invoked, for example =LAMBDA(x,x+1)(1)."
234///   - q: "Does a LAMBDA read outer LET variables at call time or definition time?"
235///     a: "Definition time. The closure captures its lexical environment when created."
236///   - q: "Can I call a LAMBDA with fewer or extra arguments?"
237///     a: "No. Invocation arity must match the declared parameter count exactly, or #VALUE! is returned."
238/// ```
239///
240/// [formualizer-docgen:schema:start]
241/// Name: LAMBDA
242/// Type: LambdaFn
243/// Min args: 1
244/// Max args: variadic
245/// Variadic: true
246/// Signature: LAMBDA(<schema unavailable>)
247/// Arg schema: <unavailable: arg_schema panicked>
248/// Caps: PURE, SHORT_CIRCUIT
249/// [formualizer-docgen:schema:end]
250impl Function for LambdaFn {
251    fn caps(&self) -> FnCaps {
252        FnCaps::PURE | FnCaps::SHORT_CIRCUIT
253    }
254
255    fn name(&self) -> &'static str {
256        "LAMBDA"
257    }
258
259    fn min_args(&self) -> usize {
260        1
261    }
262
263    fn variadic(&self) -> bool {
264        true
265    }
266
267    fn dispatch<'a, 'b, 'c>(
268        &self,
269        args: &'c [ArgumentHandle<'a, 'b>],
270        ctx: &dyn FunctionContext<'b>,
271    ) -> Result<CalcValue<'b>, ExcelError> {
272        self.eval(args, ctx)
273    }
274
275    fn eval<'a, 'b, 'c>(
276        &self,
277        args: &'c [ArgumentHandle<'a, 'b>],
278        _ctx: &dyn FunctionContext<'b>,
279    ) -> Result<CalcValue<'b>, ExcelError> {
280        if args.is_empty() {
281            return Ok(CalcValue::Scalar(LiteralValue::Error(value_error(
282                "LAMBDA requires at least a calculation expression",
283            ))));
284        }
285
286        let mut params = Vec::new();
287        let mut seen = HashSet::new();
288        for arg in &args[..args.len() - 1] {
289            let name = match local_name_from_ast(arg.ast()) {
290                Ok(name) => name,
291                Err(e) => return Ok(CalcValue::Scalar(LiteralValue::Error(e))),
292            };
293            let key = name.to_ascii_uppercase();
294            if !seen.insert(key) {
295                return Ok(CalcValue::Scalar(LiteralValue::Error(value_error(
296                    "LAMBDA parameter names must be unique",
297                ))));
298            }
299            params.push(name);
300        }
301
302        let closure = LambdaClosure {
303            params,
304            body: args[args.len() - 1].ast().clone(),
305            captured_env: args[0].current_env(),
306        };
307
308        Ok(CalcValue::Callable(Arc::new(closure)))
309    }
310}
311
312pub fn register_builtins() {
313    crate::function_registry::register_function(Arc::new(LetFn));
314    crate::function_registry::register_function(Arc::new(LambdaFn));
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320    use crate::test_workbook::TestWorkbook;
321    use formualizer_parse::parser::parse;
322
323    fn test_wb() -> TestWorkbook {
324        TestWorkbook::new()
325            .with_function(Arc::new(LetFn))
326            .with_function(Arc::new(LambdaFn))
327    }
328
329    fn eval(src: &str) -> LiteralValue {
330        eval_result(src).expect("eval")
331    }
332
333    fn eval_result(src: &str) -> Result<LiteralValue, ExcelError> {
334        eval_result_with_wb(src, test_wb())
335    }
336
337    fn eval_with_wb(src: &str, wb: TestWorkbook) -> LiteralValue {
338        eval_result_with_wb(src, wb).expect("eval")
339    }
340
341    fn eval_result_with_wb(src: &str, wb: TestWorkbook) -> Result<LiteralValue, ExcelError> {
342        let interp = wb.interpreter();
343        let ast = parse(src).expect("parse");
344        interp.evaluate_ast(&ast).map(|v| v.into_literal())
345    }
346
347    #[test]
348    fn let_binds_values() {
349        assert_eq!(eval("=LET(x,2,x+3)"), LiteralValue::Number(5.0));
350    }
351
352    #[test]
353    fn let_nested_shadowing() {
354        assert_eq!(eval("=LET(x,2,LET(x,5,x)+x)"), LiteralValue::Number(7.0));
355    }
356
357    #[test]
358    fn lambda_can_be_bound_and_invoked() {
359        assert_eq!(
360            eval("=LET(inc,LAMBDA(n,n+1),inc(41))"),
361            LiteralValue::Number(42.0)
362        );
363    }
364
365    #[test]
366    fn lambda_closure_captures_outer_bindings() {
367        assert_eq!(
368            eval("=LET(k,10,addk,LAMBDA(n,n+k),addk(5))"),
369            LiteralValue::Number(15.0)
370        );
371    }
372
373    #[test]
374    fn lambda_arity_errors() {
375        let v = eval("=LET(inc,LAMBDA(n,n+1),inc(1,2))");
376        match v {
377            LiteralValue::Error(e) => assert_eq!(e.kind, ExcelErrorKind::Value),
378            other => panic!("expected error, got {other:?}"),
379        }
380    }
381
382    #[test]
383    fn lambda_value_requires_invocation() {
384        let v = eval("=LAMBDA(x,x+1)");
385        match v {
386            LiteralValue::Error(e) => assert_eq!(e.kind, ExcelErrorKind::Calc),
387            other => panic!("expected #CALC!, got {other:?}"),
388        }
389    }
390
391    #[test]
392    fn let_rejects_non_identifier_name() {
393        let v = eval("=LET(A1,2,A1)");
394        match v {
395            LiteralValue::Error(e) => assert_eq!(e.kind, ExcelErrorKind::Value),
396            other => panic!("expected #VALUE!, got {other:?}"),
397        }
398    }
399
400    #[test]
401    fn lambda_rejects_duplicate_params() {
402        let v = eval("=LAMBDA(x,x,x+1)");
403        match v {
404            LiteralValue::Error(e) => assert_eq!(e.kind, ExcelErrorKind::Value),
405            other => panic!("expected #VALUE!, got {other:?}"),
406        }
407    }
408
409    #[test]
410    fn let_and_lambda_names_are_case_insensitive() {
411        assert_eq!(eval("=LET(x,1,X+1)"), LiteralValue::Number(2.0));
412        assert_eq!(
413            eval("=LET(F,LAMBDA(n,n+1),f(1))"),
414            LiteralValue::Number(2.0)
415        );
416    }
417
418    #[test]
419    fn let_shadows_workbook_named_range() {
420        let wb = test_wb().with_named_range("x", vec![vec![LiteralValue::Number(100.0)]]);
421        assert_eq!(eval_with_wb("=LET(X,1,x+1)", wb), LiteralValue::Number(2.0));
422    }
423
424    #[test]
425    fn lambda_param_shadows_outer_scope() {
426        assert_eq!(
427            eval("=LET(n,5,f,LAMBDA(n,n+1),f(10))"),
428            LiteralValue::Number(11.0)
429        );
430    }
431
432    #[test]
433    fn lambda_closure_snapshot_semantics() {
434        assert_eq!(
435            eval("=LET(k,1,f,LAMBDA(x,x+k),k,2,f(0))"),
436            LiteralValue::Number(1.0)
437        );
438    }
439
440    #[test]
441    fn let_undefined_symbol_before_binding_errors() {
442        let err = eval_result("=LET(x,y,y,2,x)").expect_err("expected #NAME?");
443        assert_eq!(err.kind, ExcelErrorKind::Name);
444    }
445
446    #[test]
447    fn non_invoked_lambda_in_let_is_calc_error() {
448        let v = eval("=LET(f,LAMBDA(x,x+1),f)");
449        match v {
450            LiteralValue::Error(e) => assert_eq!(e.kind, ExcelErrorKind::Calc),
451            other => panic!("expected #CALC!, got {other:?}"),
452        }
453    }
454}