Skip to content

Commit 224ec96

Browse files
committed
Tighten loop-depth guard in forward walker to prevent hangs
1 parent cdcd493 commit 224ec96

2 files changed

Lines changed: 41 additions & 24 deletions

File tree

docs/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2121

2222
### Fixed
2323

24-
- **Analyzer and LSP no longer hang on files with deeply nested loops.** Files with multiple levels of foreach/while/for inside if-branches caused exponential blowup in the forward walker's two-pass loop strategy. A unified loop-depth guard now bounds re-walks regardless of code path (diagnostics, completion, hover), preventing hangs on previously stuck files across three test projects.
24+
- **Analyzer and LSP no longer hang on files with deeply nested loops.** Files with multiple levels of foreach/while/for inside if-branches caused exponential blowup in the forward walker's two-pass loop strategy. A unified loop-depth guard now bounds re-walks regardless of code path (diagnostics, completion, hover), preventing hangs on all previously stuck files across three test projects.
2525

2626
### Changed
2727

src/completion/variable/forward_walk.rs

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1783,16 +1783,15 @@ thread_local! {
17831783
}
17841784

17851785
/// Maximum loop nesting depth at which the two-pass strategy is used.
1786-
/// Beyond this depth, loops use a single pass only. Normal PHP code
1787-
/// rarely nests loops beyond 4–5 levels; deeper nesting with the
1788-
/// two-pass strategy causes exponential blowup from if-branch merging
1789-
/// re-walking inner loop bodies (B27).
1790-
const MAX_TWO_PASS_LOOP_DEPTH: u32 = 4;
1786+
/// Beyond this depth, loops use a single pass only. At depth 2, only
1787+
/// a standalone loop or the outermost of two nested loops uses two
1788+
/// passes; deeper nesting gets a single pass.
1789+
const MAX_TWO_PASS_LOOP_DEPTH: u32 = 2;
17911790

17921791
/// Maximum loop nesting depth before foreach bails out entirely
17931792
/// (skipping even the single-pass body walk). This is the hard
17941793
/// safety net for truly pathological nesting.
1795-
const MAX_LOOP_DEPTH: u32 = 8;
1794+
const MAX_LOOP_DEPTH: u32 = 6;
17961795

17971796
/// Walk a sequence of statements top-to-bottom, updating `scope` at
17981797
/// each step. Stops when a statement's start offset reaches or exceeds
@@ -5319,6 +5318,18 @@ fn bind_foreach_key<'b>(
53195318
/// pre-loop scope, and the second pass re-walks with full visibility
53205319
/// of loop-carried assignments.
53215320
fn process_while<'b>(while_stmt: &'b While<'b>, scope: &mut ScopeState, ctx: &ForwardWalkCtx<'_>) {
5321+
let loop_depth = LOOP_DEPTH.with(|c| {
5322+
let v = c.get() + 1;
5323+
c.set(v);
5324+
v
5325+
});
5326+
5327+
// Hard limit: bail out entirely when loop depth is excessive (B27).
5328+
if loop_depth > MAX_LOOP_DEPTH {
5329+
LOOP_DEPTH.with(|c| c.set(loop_depth - 1));
5330+
return;
5331+
}
5332+
53225333
// Record `&&` chain snapshots for the while condition.
53235334
record_and_chain_snapshots(while_stmt.condition, scope, ctx);
53245335

@@ -5370,12 +5381,6 @@ fn process_while<'b>(while_stmt: &'b While<'b>, scope: &mut ScopeState, ctx: &Fo
53705381
}
53715382
}
53725383

5373-
let loop_depth = LOOP_DEPTH.with(|c| {
5374-
let v = c.get() + 1;
5375-
c.set(v);
5376-
v
5377-
});
5378-
53795384
// Skip the second pass when loop nesting is deep to avoid
53805385
// exponential blowup (B27). Applies uniformly to all paths.
53815386
if loop_depth > MAX_TWO_PASS_LOOP_DEPTH {
@@ -5432,6 +5437,18 @@ fn process_while<'b>(while_stmt: &'b While<'b>, scope: &mut ScopeState, ctx: &Fo
54325437
/// This handles the common pattern where a variable is assigned late
54335438
/// in the body but referenced (or guarded on) earlier.
54345439
fn process_for<'b>(for_stmt: &'b For<'b>, scope: &mut ScopeState, ctx: &ForwardWalkCtx<'_>) {
5440+
let loop_depth = LOOP_DEPTH.with(|c| {
5441+
let v = c.get() + 1;
5442+
c.set(v);
5443+
v
5444+
});
5445+
5446+
// Hard limit: bail out entirely when loop depth is excessive (B27).
5447+
if loop_depth > MAX_LOOP_DEPTH {
5448+
LOOP_DEPTH.with(|c| c.set(loop_depth - 1));
5449+
return;
5450+
}
5451+
54355452
// Process initializer expressions (e.g. `$i = 0`).
54365453
for init_expr in for_stmt.initializations.iter() {
54375454
process_assignment_expr(init_expr, scope, ctx);
@@ -5470,12 +5487,6 @@ fn process_for<'b>(for_stmt: &'b For<'b>, scope: &mut ScopeState, ctx: &ForwardW
54705487
}
54715488
}
54725489

5473-
let loop_depth = LOOP_DEPTH.with(|c| {
5474-
let v = c.get() + 1;
5475-
c.set(v);
5476-
v
5477-
});
5478-
54795490
// Skip the second pass when loop nesting is deep to avoid
54805491
// exponential blowup (B27). Applies uniformly to all paths.
54815492
if loop_depth > MAX_TWO_PASS_LOOP_DEPTH {
@@ -5525,17 +5536,23 @@ fn process_for<'b>(for_stmt: &'b For<'b>, scope: &mut ScopeState, ctx: &ForwardW
55255536
/// least once, so we do NOT merge with a pre-loop scope — the final
55265537
/// scope is the pass-2 result directly.
55275538
fn process_do_while<'b>(dw: &'b DoWhile<'b>, scope: &mut ScopeState, ctx: &ForwardWalkCtx<'_>) {
5528-
let pre_loop_scope = scope.clone();
5529-
5530-
// ── Pass 1: discover types assigned in the body ─────────
5531-
walk_body_forward(std::iter::once(dw.statement), scope, ctx);
5532-
55335539
let loop_depth = LOOP_DEPTH.with(|c| {
55345540
let v = c.get() + 1;
55355541
c.set(v);
55365542
v
55375543
});
55385544

5545+
// Hard limit: bail out entirely when loop depth is excessive (B27).
5546+
if loop_depth > MAX_LOOP_DEPTH {
5547+
LOOP_DEPTH.with(|c| c.set(loop_depth - 1));
5548+
return;
5549+
}
5550+
5551+
let pre_loop_scope = scope.clone();
5552+
5553+
// ── Pass 1: discover types assigned in the body ─────────
5554+
walk_body_forward(std::iter::once(dw.statement), scope, ctx);
5555+
55395556
// Skip the second pass when loop nesting is deep to avoid
55405557
// exponential blowup (B27). Applies uniformly to all paths.
55415558
if loop_depth > MAX_TWO_PASS_LOOP_DEPTH {

0 commit comments

Comments
 (0)