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