formualizer_eval/builtins/
lambda.rs1use 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
46impl 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
196impl 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}