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 {}