@@ -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.
50355086fn 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