use std::cell::Cell;
const STACK_PER_SOURCE_BYTE: usize = if cfg!(fuzzing) {
8 * 2048
} else if cfg!(debug_assertions) {
2048
} else {
1024
};
const STACK_PER_BRACKET_LEVEL: usize = if cfg!(fuzzing) { 8 * 4096 } else { 4096 };
const BASE_STACK: usize = 256 * 1024;
const MAX_STACK: usize = if cfg!(target_pointer_width = "64") {
64 << 30
} else {
1 << 30
};
thread_local! {
static KNOWN_STACK: Cell<Option<(usize, usize)>> = const { Cell::new(None) };
}
pub fn register_thread_stack(size: usize) {
let top = stack_address();
KNOWN_STACK.with(|known| known.set(Some((top, size))));
}
pub fn stack_need(source_len: usize, bracket_depth: usize) -> Option<usize> {
let need = source_len
.saturating_mul(STACK_PER_SOURCE_BYTE)
.saturating_add(bracket_depth.saturating_mul(STACK_PER_BRACKET_LEVEL))
.saturating_add(BASE_STACK);
(need <= MAX_STACK).then_some(need)
}
pub fn run_with_stack<R: Send>(need: usize, run: impl FnOnce() -> R + Send) -> R {
if remaining_stack() >= need {
return run();
}
let slot = std::sync::Mutex::new(Some(run));
let take = || {
slot.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take()
};
let spawned = std::thread::scope(|scope| {
std::thread::Builder::new()
.stack_size(need)
.spawn_scoped(scope, || {
register_thread_stack(need);
take().map(|run| run())
})
.map(|handle| {
handle
.join()
.unwrap_or_else(|panic| std::panic::resume_unwind(panic))
})
});
match spawned {
Ok(Some(result)) => result,
Ok(None) | Err(_) => match take() {
Some(run) => run(),
None => unreachable!("the spawned thread took the closure and returned its result"),
},
}
}
fn remaining_stack() -> usize {
KNOWN_STACK.with(Cell::get).map_or(0, |(top, size)| {
let used = top.saturating_sub(stack_address());
size.saturating_sub(used)
})
}
#[inline(never)]
fn stack_address() -> usize {
let marker = 0_u8;
std::ptr::from_ref(std::hint::black_box(&marker)) as usize
}
#[cfg(test)]
mod tests {
use super::*;
const PROBE_STACK: usize = 4 << 20;
fn on_registered_thread(probe: impl FnOnce() + Send + 'static) {
std::thread::Builder::new()
.stack_size(PROBE_STACK)
.spawn(move || {
register_thread_stack(PROBE_STACK);
probe();
})
.expect("probe thread should start")
.join()
.expect("probe thread should finish");
}
#[test]
fn need_grows_with_length_and_brackets() {
assert_eq!(stack_need(0, 0), Some(BASE_STACK));
assert!(stack_need(1000, 0) > stack_need(100, 0));
assert!(stack_need(1000, 500) > stack_need(1000, 0));
assert_eq!(stack_need(usize::MAX, usize::MAX), None);
assert_eq!(stack_need(MAX_STACK / STACK_PER_SOURCE_BYTE + 1, 0), None);
}
#[test]
fn unknown_thread_has_no_stack_to_spare() {
std::thread::spawn(|| assert_eq!(remaining_stack(), 0))
.join()
.expect("probe thread should finish");
}
#[test]
fn registered_thread_reports_its_stack() {
on_registered_thread(|| {
let remaining = remaining_stack();
assert!(remaining > PROBE_STACK - (1 << 20) && remaining <= PROBE_STACK);
});
}
#[test]
fn small_need_runs_on_the_current_thread() {
on_registered_thread(|| {
let caller = std::thread::current().id();
assert_eq!(
run_with_stack(1 << 20, || std::thread::current().id()),
caller
);
});
}
#[test]
fn large_need_runs_on_a_sized_thread() {
let caller = std::thread::current().id();
let (ran_on, remaining) = run_with_stack(64 << 20, || {
(std::thread::current().id(), remaining_stack())
});
assert_ne!(ran_on, caller);
assert!(remaining > 60 << 20);
}
}