@@ -693,128 +693,6 @@ so this fast-path would apply to the majority of checks.
693693
694694---
695695
696- ## P20. Forward walker: bounded iteration and depth-cap cleanup
697-
698- The forward walker's two-pass loop strategy (walk body once to
699- discover assignments, merge, walk again) interacts multiplicatively
700- with if-branch merging. This exponential blowup is currently
701- papered over with three thread-local depth counters:
702-
703- - ` LOOP_DEPTH ` / ` MAX_LOOP_DEPTH = 6 ` : hard cap on loop nesting.
704- Beyond depth 2 (` MAX_TWO_PASS_LOOP_DEPTH ` ), loops get a single
705- pass only. Beyond depth 6, the body is skipped entirely.
706- - ` WALK_DEPTH ` / ` MAX_WALK_DEPTH = 50 ` : guards ` walk_body_forward `
707- recursion across all block nesting (if/else, try/catch, switch).
708- - ` PROCESS_DEPTH ` / ` MAX_PROCESS_DEPTH = 80 ` : guards
709- ` process_statement ` recursion for the same nesting.
710-
711- All three caps silently produce incomplete type information when
712- they fire. The high values of 50 and 80 (vs real PHP nesting of
713- 10-15) indicate they exist as safety nets for the exponential
714- blowup, not for real nesting depth.
715-
716- ### How Mago solves the loop problem
717-
718- Mago (` references/mago/crates/analyzer/src/statement/loop/ ` ) uses
719- assignment-depth-bounded iteration:
720-
721- 1 . ** Assignment map.** Before analyzing a loop body, a cheap AST
722- walk (` assignment_map_visitor.rs ` , no type resolution) builds a
723- ` BTreeMap<Atom, BTreeSet<Atom>> ` of which variables depend on
724- which other variables. The function ` get_assignment_map_depth `
725- follows the dependency chain (using ` remove ` to break cycles)
726- and returns the longest chain length.
727-
728- 2 . ** Bounded re-walking.** The loop body is re-walked at most
729- ` assignment_depth ` times (typically 1-3), not proportional to
730- nesting depth. Between iterations, ` clean_nodes ` clears cached
731- expression types so the body is re-analyzed fresh.
732-
733- 3 . ** Fixed-point early exit.** After each re-walk, Mago checks
734- whether any variable's type actually changed (` has_changes `
735- flag). If types have stabilized, it breaks immediately
736- (` loop/mod.rs ` lines 561-569).
737-
738- 4 . ** Type widening.** Integer bounds are widened across iterations
739- (lines 430-494) to ensure convergence in few iterations.
740-
741- 5 . ** Union merging.** Changed types are merged via
742- ` combine_union_types ` (lines 524-533), not by re-walking
743- branches. Nesting an if inside a foreach does not multiply walks.
744-
745- Note: Mago's statement walker itself IS recursive (the ` Analyzable `
746- trait dispatches via match, calling ` .analyze() ` on sub-statements).
747- There are no depth limits and no stack size overrides. The walker
748- does not need them because the loop analysis avoids the exponential
749- blowup that makes our walker's recursion dangerous.
750-
751- PHPStan's ` NodeScopeResolver::processStmtNodes() ` is also recursive
752- with no depth counters. Phpactor's ` FrameResolver ` likewise.
753-
754- ### Deliverables
755-
756- This is a single work item with three steps:
757-
758- #### Step 1: Assignment-depth-bounded loop iteration
759-
760- Replace the ` LOOP_DEPTH ` / ` MAX_TWO_PASS_LOOP_DEPTH ` /
761- ` MAX_LOOP_DEPTH ` system with Mago-style bounded iteration:
762-
763- - Build an assignment dependency map before each loop body
764- (cheap AST walk, no type resolution).
765- - Compute assignment depth from the dependency chain.
766- - Re-walk the body at most ` assignment_depth ` times.
767- - After each re-walk, compare scope before/after. Stop when no
768- types changed (fixed-point).
769- - Consider type widening for integer literal types inside loops.
770-
771- ** Success criteria:** Remove ` LOOP_DEPTH ` , ` MAX_TWO_PASS_LOOP_DEPTH ` ,
772- and ` MAX_LOOP_DEPTH ` . No test regressions. Loop-heavy files that
773- previously hit the depth cap now produce correct types.
774-
775- #### Step 2: Verify and remove ` MAX_WALK_DEPTH ` / ` MAX_PROCESS_DEPTH `
776-
777- After Step 1 eliminates the exponential blowup, the 50/80 caps
778- should never fire on real code (PHP nesting rarely exceeds 15).
779-
780- - Run the full test suite and ` analyse ` on the largest available
781- project. Log when either cap fires.
782- - If neither fires, remove both counters entirely.
783- - If either fires on real code, investigate why (it likely indicates
784- a remaining exponential path that Step 1 missed).
785-
786- ** Success criteria:** ` WALK_DEPTH ` and ` PROCESS_DEPTH ` counters
787- removed from ` forward_walk.rs ` . No test regressions.
788-
789- #### Step 3: Verify and remove 32 MB stack threads (diagnostic workers)
790-
791- The ` analyse.rs ` Phase 2 diagnostic workers use
792- ` stack_size(32 * 1024 * 1024) ` because the forward walker's deep
793- recursion can overflow the default 8 MB stack. After Steps 1-2,
794- the recursion depth is bounded by actual PHP nesting (~ 15 levels).
795-
796- - Run the full test suite and ` analyse ` on the largest available
797- project with default stack sizes (remove or comment out the
798- ` stack_size ` calls on diagnostic worker threads).
799- - If no stack overflows occur, remove the ` stack_size ` calls.
800- - Note: the eager-population and index-worker threads also use
801- 32 MB stacks for class resolution depth (addressed by ER5
802- separately). Only remove the diagnostic worker stacks here.
803-
804- ** Success criteria:** Diagnostic worker threads in ` analyse.rs `
805- run with default stack sizes. The remaining ` stack_size ` calls
806- (eager-population, index workers, reference parsing) are tracked
807- by ER5 and P25 respectively.
808-
809- ### When to implement
810-
811- After the forward walker stabilizes (it is less than a week old).
812- The current depth guards are sufficient to prevent hangs; this item
813- improves accuracy on deeply nested loops and eliminates the safety
814- nets that mask the root cause.
815-
816- ---
817-
818696## Appendix: Profiling
819697
820698### Commands
@@ -847,8 +725,43 @@ like:
847725
848726---
849727
728+ ## P27. Remove ` WALK_DEPTH ` / ` PROCESS_DEPTH ` guards and 32 MB stack threads
729+
730+ ** Impact: Low · Effort: Low**
731+
732+ After the assignment-depth-bounded loop iteration landed (P20), the
733+ exponential blowup from loop/if-branch interaction is eliminated.
734+ The remaining depth guards in the forward walker are:
735+
736+ - ` WALK_DEPTH ` / ` MAX_WALK_DEPTH = 50 ` : guards ` walk_body_forward `
737+ recursion across all block nesting (if/else, try/catch, switch).
738+ - ` PROCESS_DEPTH ` / ` MAX_PROCESS_DEPTH = 80 ` : guards
739+ ` process_statement ` recursion for the same nesting.
740+
741+ PHP code rarely nests beyond 15 levels; these caps (50, 80) exist as
742+ safety nets and should never fire on real code now that the
743+ exponential loop path is gone.
744+
745+ Additionally, the ` analyse.rs ` Phase 2 diagnostic workers use
746+ ` stack_size(32 * 1024 * 1024) ` because the forward walker's deep
747+ recursion could overflow the default 8 MB stack. With the loop
748+ depth bounded, the recursion depth is bounded by actual PHP nesting
749+ (~ 15 levels) and default stack sizes should suffice.
750+
751+ ### Fix
752+
753+ 1 . Run the full test suite and ` analyse ` on the largest available
754+ projects with logging when either depth cap fires.
755+ 2 . If neither fires, remove both counters entirely.
756+ 3 . Remove the ` stack_size(32 * 1024 * 1024) ` calls on diagnostic
757+ worker threads in ` analyse.rs ` . Keep the eager-population and
758+ index-worker thread stacks (those are tracked by ER5 separately).
759+
760+ ---
761+
850762# Remaining anti-pattern fixes
851763
852- Most depth-cap and recursion-guard issues are addressed by P20
853- (forward walker) and ER5 (class resolution). The items below are
854- independent fixes that do not depend on either.
764+ Most remaining depth-cap issues are addressed by ER5 (class
765+ resolution). The forward walker loop iteration was addressed by the
766+ assignment-depth-bounded strategy. The items below are independent
767+ fixes that do not depend on either.
0 commit comments