Skip to main content

carta_core/
stack.rs

1//! Running recursion-heavy work on a large, dedicated stack.
2//!
3//! Some conversions recurse as deeply as their input nests (walking a document tree, decoding a
4//! deeply nested markup fragment), so a legitimately deep input can exhaust a small caller stack.
5//! [`on_deep_stack`] runs such work on a worker thread that reserves a generous stack for it.
6
7/// Virtual stack reserved for work whose recursion depth tracks input nesting. Large enough that
8/// even adversarially deep input cannot exhaust it; on demand-paged systems the reservation stays
9/// uncommitted until touched, so the cost is address space rather than resident memory.
10pub const DEEP_STACK: usize = 256 * 1024 * 1024;
11
12/// The outcome of running work on a dedicated large stack.
13#[derive(Debug)]
14pub enum DeepStack<T> {
15    /// The work ran to completion, yielding its value.
16    Completed(T),
17    /// A worker thread started but the work panicked while running on it.
18    Panicked,
19    /// No worker thread could be spawned, so the work never ran.
20    NotSpawned,
21}
22
23/// Runs `work` on a dedicated thread with a large reserved stack ([`DEEP_STACK`]), so deeply nested
24/// input cannot overflow the caller's stack.
25///
26/// The closure is consumed whether or not a worker starts, so a caller that wants to retry on the
27/// current stack expresses the work as a re-callable expression and rebuilds it in the
28/// [`DeepStack::NotSpawned`] arm.
29pub fn on_deep_stack<T, F>(work: F) -> DeepStack<T>
30where
31    T: Send,
32    F: FnOnce() -> T + Send,
33{
34    let joined = std::thread::scope(|scope| {
35        std::thread::Builder::new()
36            .stack_size(DEEP_STACK)
37            .spawn_scoped(scope, work)
38            .map(std::thread::ScopedJoinHandle::join)
39    });
40    match joined {
41        Ok(Ok(value)) => DeepStack::Completed(value),
42        Ok(Err(_)) => DeepStack::Panicked,
43        Err(_) => DeepStack::NotSpawned,
44    }
45}
46
47#[cfg(test)]
48mod tests {
49    use super::{DeepStack, on_deep_stack};
50
51    #[test]
52    fn completed_work_returns_its_value() {
53        match on_deep_stack(|| 2 + 3) {
54            DeepStack::Completed(value) => assert_eq!(value, 5),
55            _ => panic!("work should complete"),
56        }
57    }
58
59    #[test]
60    fn deep_recursion_does_not_overflow_a_small_caller_stack() {
61        fn descend(depth: usize) -> usize {
62            if depth == 0 {
63                0
64            } else {
65                1 + descend(depth - 1)
66            }
67        }
68        let outcome = std::thread::Builder::new()
69            .stack_size(64 * 1024)
70            .spawn(|| match on_deep_stack(|| descend(200_000)) {
71                DeepStack::Completed(value) => value,
72                _ => 0,
73            })
74            .expect("spawn shallow caller")
75            .join()
76            .expect("shallow caller finished");
77        assert_eq!(outcome, 200_000);
78    }
79}