mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-26 19:56:48 -06:00
Calc: a recursive LAMBDA stops at a fixed count of 140 calls
A LAMBDA recursion answered #NUM! once the formula had used 512 KiB of native stack, so the depth it reached depended on frame sizes. On a Windows release build the deepest countdown was F(F,94), where 0.7.0 reached F(F,131). The evaluator now counts the LAMBDA calls a formula has in progress, in call_lambda_values, which direct calls, defined names, MAP, REDUCE, SCAN, BYROW, BYCOL and MAKEARRAY all go through. The 141st call answers #NUM!. The countdown F(F,131), the deepest that the 400-level MAX_DEPTH allows, has 132 calls in progress and still runs. The stack check stays as a guard. Its budget rises to 896 KiB, which leaves 128 KiB of a 1 MiB thread for the frames above the formula. a_recursive_lambda_stops_at_the_same_call_on_any_stack_size runs recursions on each side of the limit on a 1 MiB and a 64 MiB thread and expects the same results. The 1 MiB test now runs the countdown F(F,131).
This commit is contained in:
+26
-5
@@ -49,10 +49,16 @@ fn err(e: CellError) -> Ev {
|
||||
Ev::V(Value::Error(e))
|
||||
}
|
||||
|
||||
/// LAMBDA calls that one formula may have in progress at once, counted the same way on every build.
|
||||
/// The next call answers `#NUM!`. A countdown from 131, the deepest that `MAX_DEPTH` allows, has 132
|
||||
/// calls in progress, so it still runs.
|
||||
const MAX_LAMBDA_CALLS: usize = 140;
|
||||
|
||||
/// Native stack, in bytes, that one formula's evaluation may use below its first `eval`. Past it,
|
||||
/// `eval` answers `#NUM!`. One level of `MAX_DEPTH` can take several stack frames. The Windows main
|
||||
/// thread and the web build have 1 MiB of stack.
|
||||
const STACK_BUDGET: usize = 512 * 1024;
|
||||
/// `eval` answers `#NUM!`. It guards the stack and leaves 128 KiB of a 1 MiB thread for the frames
|
||||
/// above the formula. A cell that another cell's formula computes on demand gets its own `Evaluator`,
|
||||
/// which starts its own budget.
|
||||
const STACK_BUDGET: usize = 896 * 1024;
|
||||
|
||||
/// The address of a local variable, which shows how far down the native stack this call is.
|
||||
#[inline(always)]
|
||||
@@ -96,6 +102,8 @@ pub struct Evaluator<'h> {
|
||||
pub at: CellRef,
|
||||
env: Vec<(String, Ev)>,
|
||||
depth: usize,
|
||||
/// LAMBDA calls in progress.
|
||||
calls: usize,
|
||||
/// `stack_position()` at the outermost `eval`.
|
||||
stack_top: usize,
|
||||
}
|
||||
@@ -135,14 +143,22 @@ impl Ctx for FnCtx<'_, '_> {
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let mut ev = Evaluator { host: &mut *self.ev.host, sheet: area.sheet, at, env, depth: self.ev.depth, stack_top: self.ev.stack_top };
|
||||
let mut ev = Evaluator {
|
||||
host: &mut *self.ev.host,
|
||||
sheet: area.sheet,
|
||||
at,
|
||||
env,
|
||||
depth: self.ev.depth,
|
||||
calls: self.ev.calls,
|
||||
stack_top: self.ev.stack_top,
|
||||
};
|
||||
Some(matches!(ev.value(&shifted).scalar().to_bool(), Ok(true)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'h> Evaluator<'h> {
|
||||
pub fn new(host: &'h mut dyn Host, sheet: usize, at: CellRef) -> Self {
|
||||
Evaluator { host, sheet, at, env: Vec::new(), depth: 0, stack_top: 0 }
|
||||
Evaluator { host, sheet, at, env: Vec::new(), depth: 0, calls: 0, stack_top: 0 }
|
||||
}
|
||||
|
||||
/// Evaluates to a value (references are dereferenced; multi-cell → array).
|
||||
@@ -963,10 +979,15 @@ impl<'h> Evaluator<'h> {
|
||||
}
|
||||
|
||||
fn call_lambda_values(&mut self, l: &Lambda, bound: Vec<(String, Ev)>) -> Ev {
|
||||
if self.calls >= MAX_LAMBDA_CALLS {
|
||||
return err(CellError::Num);
|
||||
}
|
||||
self.calls += 1;
|
||||
let saved = std::mem::replace(&mut self.env, l.env.clone());
|
||||
self.env.extend(bound);
|
||||
let r = self.eval(&l.body);
|
||||
self.env = saved;
|
||||
self.calls -= 1;
|
||||
r
|
||||
}
|
||||
|
||||
|
||||
@@ -662,7 +662,7 @@ fn a_lambda_that_calls_itself_without_end_is_num_on_a_small_stack() {
|
||||
t.set("A1", endless);
|
||||
t.set("A2", "=LOOP(1)");
|
||||
t.set("A3", "=LET(F,LAMBDA(me,n,IF(n<=0,0,1+SUM(MAP({1},LAMBDA(v,me(me,n-1)))))),F(F,100000))");
|
||||
t.set("A4", "=LET(F,LAMBDA(me,n,IF(n<=0,0,1+me(me,n-1))),F(F,50))");
|
||||
t.set("A4", "=LET(F,LAMBDA(me,n,IF(n<=0,0,1+me(me,n-1))),F(F,131))");
|
||||
let evaluated = crate::evaluate(&t.wb, 0, c("B1"), endless);
|
||||
[t.get("A1"), t.get("A2"), t.get("A3"), evaluated, t.get("A4")]
|
||||
})
|
||||
@@ -672,7 +672,35 @@ fn a_lambda_that_calls_itself_without_end_is_num_on_a_small_stack() {
|
||||
assert_eq!(endless_name, Value::Error(CellError::Num));
|
||||
assert_eq!(through_map, Value::Error(CellError::Num));
|
||||
assert_eq!(evaluated, Value::Error(CellError::Num));
|
||||
assert_eq!(countdown, Value::Number(50.0));
|
||||
assert_eq!(countdown, Value::Number(131.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_recursive_lambda_stops_at_the_same_call_on_any_stack_size() {
|
||||
let run = |stack: usize| {
|
||||
std::thread::Builder::new()
|
||||
.stack_size(stack)
|
||||
.spawn(|| {
|
||||
let mut wb = Workbook::new();
|
||||
wb.names.push(gridcraft_model::DefinedName {
|
||||
name: "CNT".into(),
|
||||
scope: None,
|
||||
formula: "LAMBDA(n,IF(n=0,0,1+CNT(n-1)))".into(),
|
||||
comment: String::new(),
|
||||
hidden: false,
|
||||
});
|
||||
// F(F,n) has n + 1 calls in progress at its deepest; W adds one more call per step.
|
||||
let tail = |n: u32| format!("=LET(F,LAMBDA(me,n,IF(n=0,0,me(me,n-1))),F(F,{n}))");
|
||||
let wrapped = |n: u32| format!("=LET(W,LAMBDA(f,k,f(f,k)),F,LAMBDA(me,n,IF(n<=0,0,1+W(me,n-1))),F(F,{n}))");
|
||||
[tail(139), tail(140), wrapped(69), wrapped(70), "=CNT(131)".to_string()].map(|f| crate::evaluate(&wb, 0, c("B1"), &f))
|
||||
})
|
||||
.expect("spawn")
|
||||
.join()
|
||||
.expect("recursion must not overflow the stack")
|
||||
};
|
||||
let small = run(1 << 20);
|
||||
assert_eq!(small, [Value::Number(0.0), Value::Error(CellError::Num), Value::Number(69.0), Value::Error(CellError::Num), Value::Number(131.0)]);
|
||||
assert_eq!(run(64 << 20), small);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user