1use std::cell::Cell;
12
13const STACK_PER_SOURCE_BYTE: usize = if cfg!(fuzzing) {
19 8 * 2048
20} else if cfg!(debug_assertions) {
21 2048
22} else {
23 1024
24};
25
26const STACK_PER_BRACKET_LEVEL: usize = if cfg!(fuzzing) { 8 * 4096 } else { 4096 };
31
32const BASE_STACK: usize = 256 * 1024;
35
36const MAX_STACK: usize = if cfg!(target_pointer_width = "64") {
44 64 << 30
45} else {
46 1 << 30
47};
48
49thread_local! {
50 static KNOWN_STACK: Cell<Option<(usize, usize)>> = const { Cell::new(None) };
53}
54
55pub fn register_thread_stack(size: usize) {
61 let top = stack_address();
62 KNOWN_STACK.with(|known| known.set(Some((top, size))));
63}
64
65pub fn stack_need(source_len: usize, bracket_depth: usize) -> Option<usize> {
68 let need = source_len
69 .saturating_mul(STACK_PER_SOURCE_BYTE)
70 .saturating_add(bracket_depth.saturating_mul(STACK_PER_BRACKET_LEVEL))
71 .saturating_add(BASE_STACK);
72 (need <= MAX_STACK).then_some(need)
73}
74
75pub fn run_with_stack<R: Send>(need: usize, run: impl FnOnce() -> R + Send) -> R {
80 if remaining_stack() >= need {
81 return run();
82 }
83 let slot = std::sync::Mutex::new(Some(run));
86 let take = || {
87 slot.lock()
88 .unwrap_or_else(std::sync::PoisonError::into_inner)
89 .take()
90 };
91 let spawned = std::thread::scope(|scope| {
92 std::thread::Builder::new()
93 .stack_size(need)
94 .spawn_scoped(scope, || {
95 register_thread_stack(need);
96 take().map(|run| run())
97 })
98 .map(|handle| {
99 handle
100 .join()
101 .unwrap_or_else(|panic| std::panic::resume_unwind(panic))
102 })
103 });
104 match spawned {
105 Ok(Some(result)) => result,
106 Ok(None) | Err(_) => match take() {
107 Some(run) => run(),
108 None => unreachable!("the spawned thread took the closure and returned its result"),
109 },
110 }
111}
112
113fn remaining_stack() -> usize {
115 KNOWN_STACK.with(Cell::get).map_or(0, |(top, size)| {
116 let used = top.saturating_sub(stack_address());
118 size.saturating_sub(used)
119 })
120}
121
122#[inline(never)]
123fn stack_address() -> usize {
124 let marker = 0_u8;
125 std::ptr::from_ref(std::hint::black_box(&marker)) as usize
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 const PROBE_STACK: usize = 4 << 20;
133
134 fn on_registered_thread(probe: impl FnOnce() + Send + 'static) {
135 std::thread::Builder::new()
136 .stack_size(PROBE_STACK)
137 .spawn(move || {
138 register_thread_stack(PROBE_STACK);
139 probe();
140 })
141 .expect("probe thread should start")
142 .join()
143 .expect("probe thread should finish");
144 }
145
146 #[test]
147 fn need_grows_with_length_and_brackets() {
148 assert_eq!(stack_need(0, 0), Some(BASE_STACK));
149 assert!(stack_need(1000, 0) > stack_need(100, 0));
150 assert!(stack_need(1000, 500) > stack_need(1000, 0));
151 assert_eq!(stack_need(usize::MAX, usize::MAX), None);
152 assert_eq!(stack_need(MAX_STACK / STACK_PER_SOURCE_BYTE + 1, 0), None);
153 }
154
155 #[test]
156 fn unknown_thread_has_no_stack_to_spare() {
157 std::thread::spawn(|| assert_eq!(remaining_stack(), 0))
158 .join()
159 .expect("probe thread should finish");
160 }
161
162 #[test]
163 fn registered_thread_reports_its_stack() {
164 on_registered_thread(|| {
165 let remaining = remaining_stack();
166 assert!(remaining > PROBE_STACK - (1 << 20) && remaining <= PROBE_STACK);
167 });
168 }
169
170 #[test]
171 fn small_need_runs_on_the_current_thread() {
172 on_registered_thread(|| {
173 let caller = std::thread::current().id();
174 assert_eq!(
175 run_with_stack(1 << 20, || std::thread::current().id()),
176 caller
177 );
178 });
179 }
180
181 #[test]
182 fn large_need_runs_on_a_sized_thread() {
183 let caller = std::thread::current().id();
184 let (ran_on, remaining) = run_with_stack(64 << 20, || {
185 (std::thread::current().id(), remaining_stack())
186 });
187 assert_ne!(ran_on, caller);
188 assert!(remaining > 60 << 20);
189 }
190}