mz_ore/stack.rs
1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License in the LICENSE file at the
6// root of this repository, or online at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Stack management utilities.
17
18use std::backtrace::Backtrace;
19use std::cell::RefCell;
20use std::error::Error;
21use std::fmt;
22
23/// The red zone is the amount of stack space that must be available on the
24/// current stack in order for [`maybe_grow`] to call the supplied closure
25/// without allocating a new stack.
26///
27/// We use a much larger red zone in debug builds because several functions have
28/// been observed to have 32KB+ stack frames when compiled without
29/// optimizations. In particular, match statements on large enums are
30/// problematic, because *each arm* of the match statement gets its own
31/// dedicated stack space. For example, consider the following function:
32///
33/// ```ignore
34/// fn big_stack(input: SomeEnum) {
35/// match input {
36/// SomeEnum::Variant1 => {
37/// let a_local = SomeBigType::new();
38/// }
39/// SomeEnum::Variant2 => {
40/// let b_local = SomeBigType::new();
41/// }
42/// // ...
43/// SomeEnum::Variant10 => {
44/// let z_local = SomeBigType::new();
45/// }
46/// }
47/// }
48/// ```
49///
50/// In debug builds, the compiler will generate a stack frame that contains
51/// space for 10 separate copies of `SomeBigType`. This can quickly result in
52/// massive stack frames for perfectly reasonable code.
53pub const STACK_RED_ZONE: usize = {
54 #[cfg(debug_assertions)]
55 {
56 1024 << 10 // 1024KiB
57 }
58 #[cfg(not(debug_assertions))]
59 {
60 64 << 10 // 64KiB
61 }
62};
63
64/// The size of any freshly allocated stacks. It was chosen to match the default
65/// stack size for threads in Rust.
66///
67/// The default stack size is larger in debug builds to correspond to the
68/// larger [`STACK_RED_ZONE`].
69pub const STACK_SIZE: usize = {
70 #[cfg(debug_assertions)]
71 {
72 16 << 20 // 16MiB
73 }
74 #[cfg(not(debug_assertions))]
75 {
76 2 << 20 // 2 MiB
77 }
78};
79
80/// Grows the stack if necessary before invoking `f`.
81///
82/// This function is intended to be called at manually instrumented points in a
83/// program where arbitrarily deep recursion is known to happen. This function
84/// will check to see if it is within `STACK_RED_ZONE` bytes of the end of the
85/// stack, and if so it will allocate a new stack of at least `STACK_SIZE`
86/// bytes.
87///
88/// The closure `f` is guaranteed to run on a stack with at least
89/// `STACK_RED_ZONE` bytes, and it will be run on the current stack if there's
90/// space available.
91///
92/// It is generally better to use [`CheckedRecursion`] to enforce a limit on the
93/// stack growth. Not all recursive code paths support returning errors,
94/// however, in which case unconditionally growing the stack with this function
95/// is still preferable to panicking.
96#[inline(always)]
97pub fn maybe_grow<F, R>(f: F) -> R
98where
99 F: FnOnce() -> R,
100{
101 stacker::maybe_grow(STACK_RED_ZONE, STACK_SIZE, f)
102}
103
104/// A trait for types which support bounded recursion to prevent stack overflow.
105///
106/// The rather odd design of this trait allows checked recursion to be added to
107/// existing mutually recursive functions without threading an explicit `depth:
108/// &mut usize` parameter through each function. As long as there is an
109/// existing context structure, or if the mutually recursive functions are
110/// methods on a context structure, the [`RecursionGuard`] can be embedded
111/// inside this existing structure.
112///
113/// # Examples
114///
115/// Consider a simple expression evaluator:
116///
117/// ```
118/// # use std::collections::BTreeMap;
119///
120/// enum Expr {
121/// Var { name: String },
122/// Add { left: Box<Expr>, right: Box<Expr> },
123/// }
124///
125/// struct Evaluator {
126/// vars: BTreeMap<String, i64>,
127/// }
128///
129/// impl Evaluator {
130/// fn eval(&mut self, expr: &Expr) -> i64 {
131/// match expr {
132/// Expr::Var { name } => self.vars[name],
133/// Expr::Add { left, right } => self.eval(left) + self.eval(right),
134/// }
135/// }
136/// }
137/// ```
138///
139/// Calling `eval` could overflow the stack and crash with a sufficiently large
140/// `expr`. This is the situation `CheckedRecursion` is designed to solve, like
141/// so:
142///
143/// ```
144/// # use std::collections::BTreeMap;
145/// # enum Expr {
146/// # Var { name: String },
147/// # Add { left: Box<Expr>, right: Box<Expr> },
148/// # }
149/// use mz_ore::stack::{CheckedRecursion, RecursionGuard, RecursionLimitError};
150///
151/// struct Evaluator {
152/// vars: BTreeMap<String, i64>,
153/// recursion_guard: RecursionGuard,
154/// }
155///
156/// impl Evaluator {
157/// fn eval(&mut self, expr: &Expr) -> Result<i64, RecursionLimitError> {
158/// // ADDED: call to `self.checked_recur`.
159/// self.checked_recur_mut(|e| match expr {
160/// Expr::Var { name } => Ok(e.vars[name]),
161/// Expr::Add { left, right } => Ok(e.eval(left)? + e.eval(right)?),
162/// })
163/// }
164/// }
165///
166/// impl CheckedRecursion for Evaluator {
167/// fn recursion_guard(&self) -> &RecursionGuard {
168/// &self.recursion_guard
169/// }
170/// }
171/// ```
172pub trait CheckedRecursion {
173 /// Extracts a reference to the recursion guard embedded within the type.
174 fn recursion_guard(&self) -> &RecursionGuard;
175
176 /// Checks whether it is safe to recur and calls `f` if so.
177 ///
178 /// If the recursion limit for the recursion guard returned by
179 /// [`CheckedRecursion::recursion_guard`] has been reached, returns a
180 /// `RecursionLimitError`. Otherwise, it will call `f`, possibly growing the
181 /// stack if necessary.
182 ///
183 /// Calls to this function must be manually inserted at any point that
184 /// mutual recursion occurs.
185 #[inline(always)]
186 fn checked_recur<F, T, E>(&self, f: F) -> Result<T, E>
187 where
188 F: FnOnce(&Self) -> Result<T, E>,
189 E: From<RecursionLimitError>,
190 {
191 self.recursion_guard().descend()?;
192 let out = maybe_grow(|| f(self));
193 self.recursion_guard().ascend();
194 out
195 }
196
197 /// Like [`CheckedRecursion::checked_recur`], but operates on a mutable
198 /// reference to `Self`.
199 #[inline(always)]
200 fn checked_recur_mut<F, T, E>(&mut self, f: F) -> Result<T, E>
201 where
202 F: FnOnce(&mut Self) -> Result<T, E>,
203 E: From<RecursionLimitError>,
204 {
205 self.recursion_guard().descend()?;
206 let out = maybe_grow(|| f(self));
207 self.recursion_guard().ascend();
208 out
209 }
210}
211
212/// Tracks recursion depth.
213///
214/// See the [`CheckedRecursion`] trait for usage instructions.
215#[derive(Default, Debug, Clone)]
216pub struct RecursionGuard {
217 depth: RefCell<usize>,
218 limit: usize,
219}
220
221impl CheckedRecursion for RecursionGuard {
222 fn recursion_guard(&self) -> &RecursionGuard {
223 self
224 }
225}
226
227impl RecursionGuard {
228 /// Constructs a new recursion guard with the specified recursion
229 /// limit.
230 pub fn with_limit(limit: usize) -> RecursionGuard {
231 RecursionGuard {
232 depth: RefCell::new(0),
233 limit,
234 }
235 }
236
237 fn descend(&self) -> Result<(), RecursionLimitError> {
238 let mut depth = self.depth.borrow_mut();
239 if *depth < self.limit {
240 *depth += 1;
241 Ok(())
242 } else {
243 let backtrace = Backtrace::force_capture();
244 Err(RecursionLimitError {
245 limit: self.limit,
246 backtrace,
247 })
248 }
249 }
250
251 fn ascend(&self) {
252 *self.depth.borrow_mut() -= 1;
253 }
254}
255
256/// A [`RecursionGuard`]'s recursion limit was reached.
257#[derive(Debug)]
258pub struct RecursionLimitError {
259 limit: usize,
260 backtrace: Backtrace,
261}
262
263impl fmt::Display for RecursionLimitError {
264 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
265 writeln!(f, "exceeded recursion limit of {}", self.limit)?;
266 writeln!(f, "backtrace:")?;
267 write!(f, "{}", self.backtrace)
268 }
269}
270
271impl Error for RecursionLimitError {}