Skip to main content

mir_analyzer/
recursion.rs

1//! Stack-overflow protection for the recursive analysis passes.
2//!
3//! Expression/statement analysis recurses along the AST (e.g. a fluent
4//! `->a()->b()->…` chain), and on-demand return-type inference nests whole
5//! body analyses of other files on the same stack. Worker threads usually have
6//! only 2 MiB, so deep input must grow the stack on the heap instead of aborting.
7
8use std::ops::ControlFlow;
9
10use php_ast::owned::visitor::{walk_owned_block, walk_owned_expr, walk_owned_stmt, OwnedVisitor};
11use php_ast::owned::{Block, Expr, Stmt};
12
13/// Stack size for analysis worker threads; matches rust-analyzer and ty.
14pub const STACK_SIZE: usize = 16 * 1024 * 1024;
15
16/// Remaining stack below which a guarded call continues on a fresh segment.
17/// Must exceed the stack used between two guards (frames are large in debug builds).
18const RED_ZONE: usize = 512 * 1024;
19const SEGMENT_SIZE: usize = 8 * 1024 * 1024;
20
21/// Parsing and the hand-written AST walkers are not guarded per node, so each
22/// analysis phase is guaranteed this much stack up front.
23const PHASE_RED_ZONE: usize = 4 * 1024 * 1024;
24const PHASE_SEGMENT_SIZE: usize = 32 * 1024 * 1024;
25
26/// Runs `f`, continuing on a heap-allocated stack segment when the current one is nearly exhausted.
27#[inline]
28pub fn ensure_stack<R>(f: impl FnOnce() -> R) -> R {
29    grow(RED_ZONE, SEGMENT_SIZE, f)
30}
31
32/// Runs `f` with enough stack for an unguarded analysis phase.
33#[inline]
34pub fn ensure_phase_stack<R>(f: impl FnOnce() -> R) -> R {
35    grow(PHASE_RED_ZONE, PHASE_SEGMENT_SIZE, f)
36}
37
38/// A parse result whose deeply nested AST is dropped on a stack large enough for the recursive `Drop`.
39pub struct GuardedParse(Option<php_rs_parser::ParseResult>);
40
41impl std::ops::Deref for GuardedParse {
42    type Target = php_rs_parser::ParseResult;
43
44    fn deref(&self) -> &Self::Target {
45        self.0.as_ref().expect("present until dropped")
46    }
47}
48
49impl Drop for GuardedParse {
50    fn drop(&mut self) {
51        let parsed = self.0.take();
52        ensure_phase_stack(|| drop(parsed));
53    }
54}
55
56/// Parses `source`, guaranteeing enough stack for deeply nested input.
57pub fn parse_versioned(source: &str, version: php_rs_parser::PhpVersion) -> GuardedParse {
58    GuardedParse(Some(ensure_phase_stack(|| {
59        php_rs_parser::parse_versioned(source, version)
60    })))
61}
62
63/// Parses `source` with the default PHP version; see [`parse_versioned`].
64pub fn parse(source: &str) -> GuardedParse {
65    GuardedParse(Some(ensure_phase_stack(|| php_rs_parser::parse(source))))
66}
67
68#[cfg(not(target_arch = "wasm32"))]
69fn grow<R>(red_zone: usize, segment: usize, f: impl FnOnce() -> R) -> R {
70    stacker::maybe_grow(red_zone, segment, f)
71}
72
73#[cfg(target_arch = "wasm32")]
74fn grow<R>(_red_zone: usize, _segment: usize, f: impl FnOnce() -> R) -> R {
75    f()
76}
77
78/// Guarded [`php_ast::owned::visitor::walk_owned_expr`]; visitors recurse through it.
79pub fn walk_expr<V: OwnedVisitor + ?Sized>(visitor: &mut V, expr: &Expr) -> ControlFlow<()> {
80    ensure_stack(|| walk_owned_expr(visitor, expr))
81}
82
83/// Guarded [`php_ast::owned::visitor::walk_owned_stmt`].
84pub fn walk_stmt<V: OwnedVisitor + ?Sized>(visitor: &mut V, stmt: &Stmt) -> ControlFlow<()> {
85    ensure_stack(|| walk_owned_stmt(visitor, stmt))
86}
87
88/// Guarded [`php_ast::owned::visitor::walk_owned_block`].
89pub fn walk_block<V: OwnedVisitor + ?Sized>(visitor: &mut V, block: &Block) -> ControlFlow<()> {
90    ensure_stack(|| walk_owned_block(visitor, block))
91}