Skip to content

Commit b6455ce

Browse files
committed
Fix infinite recursion
1 parent 2d446df commit b6455ce

2 files changed

Lines changed: 298 additions & 101 deletions

File tree

src/completion/variable/forward_walk.rs

Lines changed: 166 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -3874,6 +3874,24 @@ fn process_array_key_assignment<'b>(
38743874
scope: &mut ScopeState,
38753875
ctx: &ForwardWalkCtx<'_>,
38763876
) {
3877+
// Guard against infinite re-entry. When `resolve_rhs_with_scope`
3878+
// triggers re-evaluation of the same array key assignment (e.g.
3879+
// `$a['k'] = f($a['k'])` where reading `$a['k']` re-enters
3880+
// through the scope resolver), bail out to break the cycle.
3881+
thread_local! {
3882+
static IN_ARRAY_KEY_ASSIGN: Cell<bool> = const { Cell::new(false) };
3883+
}
3884+
if IN_ARRAY_KEY_ASSIGN.with(|c| c.get()) {
3885+
return;
3886+
}
3887+
IN_ARRAY_KEY_ASSIGN.with(|c| c.set(true));
3888+
struct ArrayKeyAssignGuard;
3889+
impl Drop for ArrayKeyAssignGuard {
3890+
fn drop(&mut self) {
3891+
IN_ARRAY_KEY_ASSIGN.with(|c| c.set(false));
3892+
}
3893+
}
3894+
let _guard = ArrayKeyAssignGuard;
38773895
// Delegate to the existing check_expression_for_assignment
38783896
// infrastructure for array key assignments. This handles
38793897
// both string-keyed shape building and generic element tracking.
@@ -4763,7 +4781,7 @@ fn assignment_map_depth(statements: &[&Statement<'_>]) -> u32 {
47634781
// one to discover the assignment, one to re-walk with the discovered
47644782
// type visible from the start. So: iterations = depth + 1.
47654783
// Clamp to a reasonable maximum to avoid pathological cases.
4766-
(max_depth + 1).min(5)
4784+
(max_depth + 1).min(3)
47674785
}
47684786

47694787
/// Recursively compute the dependency chain depth for a variable.
@@ -5031,6 +5049,39 @@ fn scopes_equal(a: &ScopeState, b: &ScopeState) -> bool {
50315049
true
50325050
}
50335051

5052+
/// Check whether the post-walk scope has any NEW or CHANGED variable
5053+
/// types compared to the pre-loop scope. This is the Mago-style
5054+
/// fixed-point check that runs BEFORE a re-walk: if nothing changed,
5055+
/// there's no point walking the body again.
5056+
///
5057+
/// Unlike `scopes_equal`, this is asymmetric: new variables in
5058+
/// `after` that weren't in `before` count as changes, but variables
5059+
/// in `before` that aren't in `after` do not (they were just not
5060+
/// assigned in the loop body).
5061+
fn scope_has_changes(before: &ScopeState, after: &ScopeState) -> bool {
5062+
for (name, after_types) in &after.locals {
5063+
match before.locals.get(name) {
5064+
None => {
5065+
// New variable assigned in the loop body.
5066+
if !after_types.is_empty() {
5067+
return true;
5068+
}
5069+
}
5070+
Some(before_types) => {
5071+
if after_types.len() != before_types.len() {
5072+
return true;
5073+
}
5074+
for (at, bt) in after_types.iter().zip(before_types.iter()) {
5075+
if at.type_string != bt.type_string {
5076+
return true;
5077+
}
5078+
}
5079+
}
5080+
}
5081+
}
5082+
false
5083+
}
5084+
50345085
/// Process a `foreach` statement.
50355086
fn process_foreach<'b>(foreach: &'b Foreach<'b>, scope: &mut ScopeState, ctx: &ForwardWalkCtx<'_>) {
50365087
let loop_depth = enter_loop();
@@ -5121,23 +5172,58 @@ fn process_foreach<'b>(foreach: &'b Foreach<'b>, scope: &mut ScopeState, ctx: &F
51215172

51225173
// ── Assignment-depth-bounded loop iteration ─────────────────
51235174
//
5124-
// Compute the assignment dependency depth of the loop body to
5125-
// determine how many iterations are needed for types to propagate
5126-
// through the entire chain. Then iterate with fixed-point
5127-
// detection: stop early when scope no longer changes.
5175+
// Walk the body once (always needed). Then check whether any
5176+
// variable types changed compared to the pre-loop scope. Only
5177+
// re-walk if there are actual changes AND the assignment depth
5178+
// requires further propagation. This matches Mago's approach:
5179+
// the fixed-point check happens BEFORE the expensive re-walk,
5180+
// not after.
51285181
let body_stmts: Vec<&Statement<'b>> = match &foreach.body {
51295182
ForeachBody::Statement(inner) => vec![*inner],
51305183
ForeachBody::ColonDelimited(body) => body.statements.iter().collect(),
51315184
};
5132-
let max_iterations = clamp_iterations_for_depth(assignment_map_depth(&body_stmts), loop_depth);
5185+
let assignment_depth = clamp_iterations_for_depth(assignment_map_depth(&body_stmts), loop_depth);
51335186

5134-
for iteration in 0..max_iterations {
5135-
let scope_before = scope.clone();
5136-
let walk_ctx = if iteration + 1 < max_iterations {
5137-
&discovery_ctx
5138-
} else {
5139-
ctx
5140-
};
5187+
// ── Initial walk (always performed) ─────────────────────────
5188+
let initial_ctx = if assignment_depth > 1 { &discovery_ctx } else { ctx };
5189+
match &foreach.body {
5190+
ForeachBody::Statement(inner) => {
5191+
walk_body_forward(std::iter::once(*inner), scope, initial_ctx);
5192+
}
5193+
ForeachBody::ColonDelimited(body) => {
5194+
walk_body_forward(body.statements.iter(), scope, initial_ctx);
5195+
}
5196+
}
5197+
5198+
// ── Re-walk iterations (only if types changed) ──────────────
5199+
for iteration in 0..assignment_depth.saturating_sub(1) {
5200+
// Check for changes BEFORE re-walking: compare post-walk
5201+
// scope against the pre-loop scope. If no variable has a
5202+
// type that differs from what was known before the loop,
5203+
// there's nothing new to propagate — skip the re-walk.
5204+
if !scope_has_changes(&pre_loop_scope, scope) {
5205+
break;
5206+
}
5207+
5208+
// Merge discovered types back into the pre-loop scope and
5209+
// re-bind foreach variables for the next iteration.
5210+
let mut next_scope = pre_loop_scope.clone();
5211+
next_scope.merge_branch(scope);
5212+
match &foreach.target {
5213+
ForeachTarget::Value(val) => {
5214+
bind_foreach_value(val.value, &iter_type, &mut next_scope, ctx);
5215+
}
5216+
ForeachTarget::KeyValue(kv) => {
5217+
bind_foreach_key(kv.key, &iter_type, &mut next_scope, ctx);
5218+
bind_foreach_value(kv.value, &iter_type, &mut next_scope, ctx);
5219+
}
5220+
}
5221+
*scope = next_scope;
5222+
5223+
// Use the real context on the final iteration so diagnostic
5224+
// snapshots and cursor handling are correct.
5225+
let is_final = iteration + 1 >= assignment_depth.saturating_sub(1);
5226+
let walk_ctx = if is_final { ctx } else { &discovery_ctx };
51415227

51425228
match &foreach.body {
51435229
ForeachBody::Statement(inner) => {
@@ -5147,28 +5233,6 @@ fn process_foreach<'b>(foreach: &'b Foreach<'b>, scope: &mut ScopeState, ctx: &F
51475233
walk_body_forward(body.statements.iter(), scope, walk_ctx);
51485234
}
51495235
}
5150-
5151-
// Fixed-point: stop if scope did not change.
5152-
if scopes_equal(scope, &scope_before) {
5153-
break;
5154-
}
5155-
5156-
// Prepare for the next iteration: merge discovered types back
5157-
// into the pre-loop scope and re-bind foreach variables.
5158-
if iteration + 1 < max_iterations {
5159-
let mut next_scope = pre_loop_scope.clone();
5160-
next_scope.merge_branch(scope);
5161-
match &foreach.target {
5162-
ForeachTarget::Value(val) => {
5163-
bind_foreach_value(val.value, &iter_type, &mut next_scope, ctx);
5164-
}
5165-
ForeachTarget::KeyValue(kv) => {
5166-
bind_foreach_key(kv.key, &iter_type, &mut next_scope, ctx);
5167-
bind_foreach_value(kv.value, &iter_type, &mut next_scope, ctx);
5168-
}
5169-
}
5170-
*scope = next_scope;
5171-
}
51725236
}
51735237

51745238
// The iterable might be empty, so the loop body might not execute
@@ -5633,15 +5697,34 @@ fn process_while<'b>(while_stmt: &'b While<'b>, scope: &mut ScopeState, ctx: &Fo
56335697
WhileBody::Statement(inner) => vec![*inner],
56345698
WhileBody::ColonDelimited(body) => body.statements.iter().collect(),
56355699
};
5636-
let max_iterations = clamp_iterations_for_depth(assignment_map_depth(&body_stmts), loop_depth);
5700+
let assignment_depth = clamp_iterations_for_depth(assignment_map_depth(&body_stmts), loop_depth);
56375701

5638-
for iteration in 0..max_iterations {
5639-
let scope_before = scope.clone();
5640-
let walk_ctx = if iteration + 1 < max_iterations {
5641-
&discovery_ctx
5642-
} else {
5643-
ctx
5644-
};
5702+
// ── Initial walk (always performed) ─────────────────────────
5703+
let initial_ctx = if assignment_depth > 1 { &discovery_ctx } else { ctx };
5704+
match &while_stmt.body {
5705+
WhileBody::Statement(inner) => {
5706+
walk_body_forward(std::iter::once(*inner), scope, initial_ctx);
5707+
}
5708+
WhileBody::ColonDelimited(body) => {
5709+
walk_body_forward(body.statements.iter(), scope, initial_ctx);
5710+
}
5711+
}
5712+
5713+
// ── Re-walk iterations (only if types changed) ──────────────
5714+
for iteration in 0..assignment_depth.saturating_sub(1) {
5715+
if !scope_has_changes(&pre_loop_scope, scope) {
5716+
break;
5717+
}
5718+
5719+
let mut next_scope = pre_loop_scope.clone();
5720+
next_scope.merge_branch(scope);
5721+
apply_condition_narrowing(while_stmt.condition, &mut next_scope, ctx);
5722+
process_condition_assignment(while_stmt.condition, &mut next_scope, ctx);
5723+
seed_pass_by_ref_in_condition(while_stmt.condition, &mut next_scope, ctx);
5724+
*scope = next_scope;
5725+
5726+
let is_final = iteration + 1 >= assignment_depth.saturating_sub(1);
5727+
let walk_ctx = if is_final { ctx } else { &discovery_ctx };
56455728

56465729
match &while_stmt.body {
56475730
WhileBody::Statement(inner) => {
@@ -5651,22 +5734,6 @@ fn process_while<'b>(while_stmt: &'b While<'b>, scope: &mut ScopeState, ctx: &Fo
56515734
walk_body_forward(body.statements.iter(), scope, walk_ctx);
56525735
}
56535736
}
5654-
5655-
// Fixed-point: stop if scope did not change.
5656-
if scopes_equal(scope, &scope_before) {
5657-
break;
5658-
}
5659-
5660-
// Prepare for the next iteration: merge discovered types back
5661-
// into the pre-loop scope and re-apply condition processing.
5662-
if iteration + 1 < max_iterations {
5663-
let mut next_scope = pre_loop_scope.clone();
5664-
next_scope.merge_branch(scope);
5665-
apply_condition_narrowing(while_stmt.condition, &mut next_scope, ctx);
5666-
process_condition_assignment(while_stmt.condition, &mut next_scope, ctx);
5667-
seed_pass_by_ref_in_condition(while_stmt.condition, &mut next_scope, ctx);
5668-
*scope = next_scope;
5669-
}
56705737
}
56715738

56725739
// When the cursor is inside the loop body (completion path), keep
@@ -5734,15 +5801,34 @@ fn process_for<'b>(for_stmt: &'b For<'b>, scope: &mut ScopeState, ctx: &ForwardW
57345801
ForBody::Statement(inner) => vec![*inner],
57355802
ForBody::ColonDelimited(body) => body.statements.iter().collect(),
57365803
};
5737-
let max_iterations = clamp_iterations_for_depth(assignment_map_depth(&body_stmts), loop_depth);
5804+
let assignment_depth = clamp_iterations_for_depth(assignment_map_depth(&body_stmts), loop_depth);
57385805

5739-
for iteration in 0..max_iterations {
5740-
let scope_before = scope.clone();
5741-
let walk_ctx = if iteration + 1 < max_iterations {
5742-
&discovery_ctx
5743-
} else {
5744-
ctx
5745-
};
5806+
// ── Initial walk (always performed) ─────────────────────────
5807+
let initial_ctx = if assignment_depth > 1 { &discovery_ctx } else { ctx };
5808+
match &for_stmt.body {
5809+
ForBody::Statement(inner) => {
5810+
walk_body_forward(std::iter::once(*inner), scope, initial_ctx);
5811+
}
5812+
ForBody::ColonDelimited(body) => {
5813+
walk_body_forward(body.statements.iter(), scope, initial_ctx);
5814+
}
5815+
}
5816+
5817+
// ── Re-walk iterations (only if types changed) ──────────────
5818+
for iteration in 0..assignment_depth.saturating_sub(1) {
5819+
if !scope_has_changes(&pre_loop_scope, scope) {
5820+
break;
5821+
}
5822+
5823+
let mut next_scope = pre_loop_scope.clone();
5824+
next_scope.merge_branch(scope);
5825+
for init_expr in for_stmt.initializations.iter() {
5826+
process_assignment_expr(init_expr, &mut next_scope, ctx);
5827+
}
5828+
*scope = next_scope;
5829+
5830+
let is_final = iteration + 1 >= assignment_depth.saturating_sub(1);
5831+
let walk_ctx = if is_final { ctx } else { &discovery_ctx };
57465832

57475833
match &for_stmt.body {
57485834
ForBody::Statement(inner) => {
@@ -5752,22 +5838,6 @@ fn process_for<'b>(for_stmt: &'b For<'b>, scope: &mut ScopeState, ctx: &ForwardW
57525838
walk_body_forward(body.statements.iter(), scope, walk_ctx);
57535839
}
57545840
}
5755-
5756-
// Fixed-point: stop if scope did not change.
5757-
if scopes_equal(scope, &scope_before) {
5758-
break;
5759-
}
5760-
5761-
// Prepare for the next iteration: merge discovered types back
5762-
// into the pre-loop scope and re-apply initializers.
5763-
if iteration + 1 < max_iterations {
5764-
let mut next_scope = pre_loop_scope.clone();
5765-
next_scope.merge_branch(scope);
5766-
for init_expr in for_stmt.initializations.iter() {
5767-
process_assignment_expr(init_expr, &mut next_scope, ctx);
5768-
}
5769-
*scope = next_scope;
5770-
}
57715841
}
57725842

57735843
// The loop body might not execute at all (condition false on
@@ -5800,29 +5870,24 @@ fn process_do_while<'b>(dw: &'b DoWhile<'b>, scope: &mut ScopeState, ctx: &Forwa
58005870

58015871
// ── Assignment-depth-bounded loop iteration ─────────────────
58025872
let body_stmts: Vec<&Statement<'b>> = vec![dw.statement];
5803-
let max_iterations = clamp_iterations_for_depth(assignment_map_depth(&body_stmts), loop_depth);
5873+
let assignment_depth = clamp_iterations_for_depth(assignment_map_depth(&body_stmts), loop_depth);
58045874

5805-
for iteration in 0..max_iterations {
5806-
let scope_before = scope.clone();
5875+
// ── Initial walk (always performed) ─────────────────────────
5876+
walk_body_forward(std::iter::once(dw.statement), scope, ctx);
58075877

5808-
walk_body_forward(std::iter::once(dw.statement), scope, ctx);
5809-
5810-
// Fixed-point: stop if scope did not change.
5811-
if scopes_equal(scope, &scope_before) {
5878+
// ── Re-walk iterations (only if types changed) ──────────────
5879+
for _iteration in 0..assignment_depth.saturating_sub(1) {
5880+
if !scope_has_changes(&pre_loop_scope, scope) {
58125881
break;
58135882
}
58145883

5815-
// Prepare for the next iteration: merge discovered types back
5816-
// into the pre-loop scope and process the condition (evaluated
5817-
// after the body, so assignments and pass-by-ref seeding from
5818-
// it should be visible on iteration >= 2).
5819-
if iteration + 1 < max_iterations {
5820-
let mut next_scope = pre_loop_scope.clone();
5821-
next_scope.merge_branch(scope);
5822-
process_condition_assignment(dw.condition, &mut next_scope, ctx);
5823-
seed_pass_by_ref_in_condition(dw.condition, &mut next_scope, ctx);
5824-
*scope = next_scope;
5825-
}
5884+
let mut next_scope = pre_loop_scope.clone();
5885+
next_scope.merge_branch(scope);
5886+
process_condition_assignment(dw.condition, &mut next_scope, ctx);
5887+
seed_pass_by_ref_in_condition(dw.condition, &mut next_scope, ctx);
5888+
*scope = next_scope;
5889+
5890+
walk_body_forward(std::iter::once(dw.statement), scope, ctx);
58265891
}
58275892

58285893
leave_loop(loop_depth);

0 commit comments

Comments
 (0)