Guides
LET/LAMBDA and Callables
Current implemented semantics for LET/LAMBDA, including invocation rules, arity, and scope behavior.
This guide reflects current behavior in formualizer-eval tests (builtins/lambda.rs and engine/tests/let_lambda.rs).
Implemented semantics
LETrequires name/value pairs plus one final expression (>= 3args and odd argument count).LAMBDAreturns a callable value; a callable used as a value (not invoked) yields#CALC!.- Invocation arity is exact (
LAMBDA(n, ...)called with 0 or 2 args returns#VALUE!). - Parameter names must be unique inside
LAMBDA(#VALUE!on duplicates). - Local names are case-insensitive in both
LETandLAMBDAinvocation (xandXare the same binding). - Local
LETbindings shadow workbook-defined names.
Edge cases you should rely on today
=LET(x,2,x+3) -> 5
=LET(inc,LAMBDA(n,n+1),inc(41)) -> 42
=LAMBDA(x,x+1) -> #CALC!
=LET(f,LAMBDA(x,x+1),f) -> #CALC!
=LET(inc,LAMBDA(n,n+1),inc(1,2)) -> #VALUE!
=LET(X,1,x+1) -> 2Shadowing and capture notes
- Parameter shadowing works:
=LET(n,5,f,LAMBDA(n,n+1),f(10))evaluates to11. - Closures capture the environment at lambda creation time (snapshot behavior):
=LET(k,1,f,LAMBDA(x,x+k),k,2,f(0))evaluates to1, not2.
- Referencing a symbol before it is bound in
LETresolves as#NAME?.
Minimal engine parity example (Rust)
use formualizer_common::{ExcelErrorKind, LiteralValue};
use formualizer_workbook::Workbook;
let mut wb = Workbook::new();
wb.add_sheet("Sheet1")?;
wb.set_formula("Sheet1", 1, 1, "=LET(inc,LAMBDA(n,n+1),inc(41))")?;
wb.set_formula("Sheet1", 1, 2, "=LET(f,LAMBDA(x,x+1),f)")?;
assert_eq!(wb.evaluate_cell("Sheet1", 1, 1)?, LiteralValue::Number(42.0));
match wb.evaluate_cell("Sheet1", 1, 2)? {
LiteralValue::Error(err) => assert_eq!(err.kind, ExcelErrorKind::Calc),
other => panic!("expected #CALC!, got {other:?}"),
}