Skip to content

fix: trampoline getSideEffectsConnectionState to avoid stack overflow on deep import chains#20993

Merged
alexander-akait merged 2 commits into
mainfrom
claude/issue-20986-CIh26
May 20, 2026
Merged

fix: trampoline getSideEffectsConnectionState to avoid stack overflow on deep import chains#20993
alexander-akait merged 2 commits into
mainfrom
claude/issue-20986-CIh26

Conversation

@alexander-akait

@alexander-akait alexander-akait commented May 20, 2026

Copy link
Copy Markdown
Member

The recursive descent through
HarmonyImportSideEffectDependency.getModuleEvaluationSideEffectsState
adds two stack frames per module and overflows V8's stack on long
chains of side-effect-free imports — surfacing as
RangeError: Maximum call stack size exceeded at the
HarmonyImportSideEffectDependency frame (issue #20986).

fixes #20986

…ow on deep import chains

The recursive descent through
`HarmonyImportSideEffectDependency.getModuleEvaluationSideEffectsState`
adds two stack frames per module and overflows V8's stack on long
chains of side-effect-free imports — surfacing as
`RangeError: Maximum call stack size exceeded` at the
HarmonyImportSideEffectDependency frame (issue #20986).

The recursive code itself wasn't introduced in 5.107.0, but #20723's
expanded `isPure` analysis (treating ArrayExpression, ObjectExpression,
NewExpression, etc. as pure) flipped many more modules to
`buildMeta.sideEffectFree`, putting them inside the recursive walk for
the first time and exposing the latent overflow.

Move the algorithm into a file-scope generator `walkSideEffects` that
`yield`s a child generator for each side-effect-free
HarmonyImportSideEffectDependency instead of recursing into
`getSideEffectsConnectionState`. The method itself is now a small
trampoline driving the generator stack. The generator body is the same
shape as the previous recursive code, so semantics and tree-shaking
results are unchanged.

Tests:
- Unit tests in `test/NormalModule.unittest.js` cover a 20000-deep
  chain, a cycle, and bailout propagation.
- Integration case `test/configCases/side-effects/deep-import-chain`
  mirrors the structure of the reporter's reproduction repo
  (https://github.com/abecirovic-mo/webpack-5.107.0-repro) with a
  500-module chain that fits within the per-case compile budget.
Copilot AI review requested due to automatic review settings May 20, 2026 18:20
@changeset-bot

changeset-bot Bot commented May 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 18d35bc

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
webpack Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

This PR is packaged and the instant preview is available (c8d97e2).

Install it locally:

  • npm
npm i -D webpack@https://pkg.pr.new/webpack@c8d97e2
  • yarn
yarn add -D webpack@https://pkg.pr.new/webpack@c8d97e2
  • pnpm
pnpm add -D webpack@https://pkg.pr.new/webpack@c8d97e2

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses a stack overflow in side-effects analysis on very deep chains of side-effect-free imports by replacing recursive descent in NormalModule.getSideEffectsConnectionState with an iterative trampoline driving a generator-based walk (fixing issue #20986) while aiming to preserve existing tree-shaking semantics.

Changes:

  • Refactor side-effects connection state evaluation to an iterative trampoline + generator walk to avoid V8 call stack overflows.
  • Add unit tests covering a 20,000-deep chain, cycle handling, and bailout propagation.
  • Add an integration config case that generates a ~500-module import chain and verifies compilation doesn’t overflow the stack; include a changeset entry.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
lib/NormalModule.js Replaces recursive side-effects traversal with a generator + trampoline approach; preserves bailout behavior.
test/NormalModule.unittest.js Adds focused unit coverage for deep-chain traversal, cycles, and bailout propagation.
test/configCases/side-effects/deep-import-chain/webpack.config.js Generates a long, side-effect-free import chain fixture for integration coverage.
test/configCases/side-effects/deep-import-chain/index.js Integration assertion for compiling the deep import chain without stack overflow.
test/configCases/side-effects/deep-import-chain/.gitignore Ignores generated src/ fixture files.
.changeset/deep-side-effect-chain-stack-overflow.md Documents the patch-level fix for the stack overflow.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread test/configCases/side-effects/deep-import-chain/index.js Outdated
@codecov

codecov Bot commented May 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.91%. Comparing base (4657874) to head (18d35bc).
⚠️ Report is 8 commits behind head on main.

❌ Your changes status has failed because you have indirect coverage changes. Learn more about Unexpected Coverage Changes and reasons for indirect coverage changes.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #20993      +/-   ##
==========================================
- Coverage   90.94%   90.91%   -0.04%     
==========================================
  Files         573      573              
  Lines       58938    58967      +29     
  Branches    15889    15894       +5     
==========================================
+ Hits        53603    53607       +4     
- Misses       5335     5360      +25     
Flag Coverage Δ
integration 89.68% <100.00%> (-0.04%) ⬇️
test262 45.34% <88.00%> (-0.03%) ⬇️
unit 36.63% <82.00%> (+0.05%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@codspeed-hq

codspeed-hq Bot commented May 20, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 43.91%

⚡ 12 improved benchmarks
❌ 3 regressed benchmarks
✅ 129 untouched benchmarks
⏩ 72 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory benchmark "asset-modules-source", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 399.1 KB 517.2 KB -22.84%
Memory benchmark "cache-filesystem", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 165.5 KB 305.9 KB -45.88%
Memory benchmark "many-modules-esm", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 143.6 KB 278 KB -48.34%
Memory benchmark "future-defaults", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 284.4 KB 152.7 KB +86.2%
Memory benchmark "many-chunks-commonjs", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 246.5 KB 174.1 KB +41.54%
Memory benchmark "many-chunks-esm", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 254.2 KB 168.9 KB +50.56%
Memory benchmark "side-effects-reexport", scenario '{"name":"mode-development","mode":"development"}' 4.8 MB 3.9 MB +25%
Memory benchmark "side-effects-reexport", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 859.5 KB 411.5 KB ×2.1
Memory benchmark "many-chunks-esm", scenario '{"name":"mode-production","mode":"production"}' 9.2 MB 7.7 MB +20.16%
Memory benchmark "concatenate-modules", scenario '{"name":"mode-development","mode":"development"}' 1,112.4 KB 785.3 KB +41.66%
Memory benchmark "asset-modules-bytes", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 323.1 KB 133.8 KB ×2.4
Memory benchmark "react", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 395.5 KB 138.3 KB ×2.9
Memory benchmark "wasm-modules-sync", scenario '{"name":"mode-production","mode":"production"}' 8 MB 6.1 MB +30.57%
Memory benchmark "many-chunks-commonjs", scenario '{"name":"mode-production","mode":"production"}' 11.2 MB 7.3 MB +54.35%
Memory benchmark "context-esm", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 665.2 KB 149.8 KB ×4.4

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing claude/issue-20986-CIh26 (18d35bc) with main (3dd0b3b)

Open in CodSpeed

Footnotes

  1. 72 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@github-actions

Copy link
Copy Markdown
Contributor

Types Coverage

Coverage after merging claude/issue-20986-CIh26 into main will be
98.96%
Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
bin
   webpack.js98.77%100%100%98.77%91
examples
   build-common.js100%100%100%100%
   buildAll.js100%100%100%100%
   examples.js100%100%100%100%
   template-common.js98.21%100%100%98.21%72
examples/custom-javascript-parser
   test.filter.js100%100%100%100%
examples/custom-javascript-parser/internals
   acorn-parse.js100%100%100%100%
   meriyah-parse.js100%100%100%100%
   oxc-parse.js91.30%100%100%91.30%140, 142–143, 145, 147, 153–154, 161, 168, 90
examples/markdown
   webpack.config.mjs100%100%100%100%
examples/typescript
   test.filter.js100%100%100%100%
examples/typescript-non-erasable
   test.filter.js50%100%100%50%5
examples/virtual-modules
   test.filter.js100%100%100%100%
examples/wasm-bindgen-esm
   test.filter.js100%100%100%100%
examples/wasm-complex
   test.filter.js100%100%100%100%
examples/wasm-simple
   test.filter.js100%100%100%100%
examples/wasm-simple-source-phase
   test.filter.js100%100%100%100%
lib
   APIPlugin.js100%100%100%100%
   AsyncDependenciesBlock.js100%100%100%100%
   AutomaticPrefetchPlugin.js100%100%100%100%
   BannerPlugin.js100%100%100%100%
   Cache.js98.21%100%100%98.21%101
   CacheFacade.js100%100%100%100%
   Chunk.js99.72%100%100%99.72%39
   ChunkGraph.js100%100%100%100%
   ChunkGroup.js100%100%100%100%
   ChunkTemplate.js100%100%100%100%
   CleanPlugin.js99.15%100%100%99.15%206, 226
   CodeGenerationResults.js100%100%100%100%
   CompatibilityPlugin.js100%100%100%100%
   Compilation.js98.45%100%100%98.45%1572, 1868, 1875, 1883, 1905, 2801, 3226, 3888, 3917, 3970–3971, 3975, 3980, 3996–3997, 4011–4012, 4017–4018, 4495, 4521, 511, 516, 5229, 5261, 5278, 5294, 5310, 5325, 5350–5351, 5353, 5681, 5686, 5692, 5695, 5707, 5709, 5713, 5729, 5744, 5776, 5830, 5854, 5968, 730–731
   Compiler.js99.55%100%100%99.55%1116–1117, 1125
   ConcatenationScope.js98.59%100%100%98.59%189
   ConditionalInitFragment.js100%100%100%100%
   ConstPlugin.js100%100%100%100%
   ContextExclusionPlugin.js100%100%100%100%
   ContextModule.js100%100%100%100%
   ContextModuleFactory.js97.75%100%100%97.75%258, 393, 418, 443, 447, 458
   ContextReplacementPlugin.js100%100%100%100%
   DefinePlugin.js98.92%100%100%98.92%158–159, 175, 194, 268
   DependenciesBlock.js100%100%100%100%
   Dependency.js98.20%100%100%98.20%379, 425
   DependencyTemplate.js100%100%100%100%
   DependencyTemplates.js100%100%100%100%
   DotenvPlugin.js98.41%100%100%98.41%378, 391–392
   DynamicEntryPlugin.js100%100%100%100%
   EntryOptionPlugin.js100%100%100%100%
   EntryPlugin.js100%100%100%100%
   Entrypoint.js100%100%100%100%
   EnvironmentPlugin.js97.14%100%100%97.14%49
   ErrorHelpers.js100%100%100%100%
   EvalDevToolModulePlugin.js100%100%100%100%
   EvalSourceMapDevToolPlugin.js100%100%100%100%
   ExportsInfo.js100%100%100%100%
   ExportsInfoApiPlugin.js100%100%100%100%
   ExternalModule.js98.97%100%100%98.97%425–429, 577
   ExternalModuleFactoryPlugin.js100%100%100%100%
   ExternalsPlugin.js100%100%100%100%
   FileSystemInfo.js99.50%100%100%99.50%182, 2252–2253, 2256, 2267, 2278, 2289, 278, 3694, 3709, 3733
   FlagAllModulesAsUsedPlugin.js100%100%100%100%
   FlagDependencyExportsPlugin.js98.74%100%100%98.74%399, 401, 405
   FlagDependencyUsagePlugin.js100%100%100%100%
   FlagEntryExportAsUsedPlugin.js100%100%100%100%
   Generator.js100%100%100%100%
   HotModuleReplacementPlugin.js100%100%100%100%
   HotUpdateChunk.js100%100%100%100%
   IgnorePlugin.js100%100%100%100%
   IgnoreWarningsPlugin.js100%100%100%100%
   InitFragment.js100%100%100%100%
   JavascriptMetaInfoPlugin.js100%100%100%100%
   LibraryTemplatePlugin.js100%100%100%100%
   LoaderOptionsPlugin.js100%100%100%100%
   LoaderTargetPlugin.js100%100%100%100%
   MainTemplate.js100%100%100%100%
   ManifestPlugin.js100%100%100%100%
   Module.js98.50%100%100%98.50%1305, 1310, 1371, 1385, 1447, 1456
   ModuleFactory.js100%100%100%100%
   ModuleFilenameHelpers.js98.85%100%100%98.85%106, 108
   ModuleGraph.js99.73%100%100%99.73%1004
   ModuleGraphConnection.js100%100%100%100%
   ModuleInfoHeaderPlugin.js100%100%100%100%
   ModuleNotFoundError.js100%100%100%100%
   ModuleProfile.js100%100%100%100%
   ModuleSourceTypeConstants.js100%100%100%100%
   ModuleTemplate.js100%100%100%100%
   ModuleTypeConstants.js100%100%100%100%
   MultiCompiler.js99.69%100%100%99.69%645
   MultiStats.js100%100%100%100%
   MultiWatching.js100%100%100%100%
   NoEmitOnErrorsPlugin.js100%100%100%100%
   NodeStuffPlugin.js100%100%100%100%
   NormalModule.js97.83%100%100%97.83%1067, 1101, 1117, 1204, 1829, 1834–1844, 789, 792, 809, 826
   NormalModuleFactory.js99.47%100%100%99.47%1075, 1384, 474, 486
   NormalModuleReplacementPlugin.js100%100%100%100%
   NullFactory.js100%100%100%100%
   OptimizationStages.js100%100%100%100%
   OptionsApply.js100%100%100%100%
   Parser.js100%100%100%100%
   PlatformPlugin.js100%100%100%100%
   PrefetchPlugin.js100%100%100%100%
   ProgressPlugin.js98.85%100%100%98.85%519–520, 525, 527, 591
   ProvidePlugin.js100%100%100%100%
   RawModule.js100%100%100%100%
   RecordIdsPlugin.js100%100%100%100%
   RequestShortener.js100%100%100%100%
   ResolverFactory.js100%100%100%100%
   RuntimeGlobals.js100%100%100%100%
   RuntimeModule.js100%100%100%100%
   RuntimePlugin.js100%100%100%100%
   RuntimeTemplate.js100%100%100%100%
   SelfModuleFactory.js100%100%100%100%
   SingleEntryPlugin.js100%100%100%100%
   SourceMapDevToolModuleOptionsPlugin.js100%100%100%100%
   SourceMapDevToolPlugin.js99.16%100%100%99.16%267–268, 610
   Stats.js100%100%100%100%
   Template.js100%100%100%100%
   TemplatedPathPlugin.js98.86%100%100%98.86%136–137
   UseStrictPlugin.js100%100%100%100%
   WarnCaseSensitiveModulesPlugin.js100%100%100%100%
   WarnDeprecatedOptionPlugin.js100%100%100%100%
   WarnNoModeSetPlugin.js100%100%100%100%
   WatchIgnorePlugin.js100%100%100%100%
   Watching.js100%100%100%100%
   WebpackError.js100%100%100%100%
   WebpackIsIncludedPlugin.js100%100%100%100%
   WebpackOptionsApply.js100%100%100%100%
   WebpackOptionsDefaulter.js100%100%100%100%
   buildChunkGraph.js99.87%100%100%99.87%325
   cli.js98.46%100%100%98.46%10, 119, 471, 503, 545, 815
   index.js99.72%100%100%99.72%165
   validateSchema.js94.67%100%100%94.67%100, 87, 89, 98
   webpack.js96.33%100%100%96.33%10, 198, 220, 222
lib/asset
   AssetBytesGenerator.js100%100%100%100%
   AssetBytesParser.js100%100%100%100%
   AssetGenerator.js100%100%100%100%
   AssetModulesPlugin.js97.33%100%100%97.33%287, 311, 314, 366, 37, 42
   AssetParser.js100%100%100%100%
   AssetSourceGenerator.js100%100%100%100%
   AssetSourceParser.js100%100%100%100%
   RawDataUrlModule.js100%100%100%100%
lib/async-modules
   AsyncModuleHelpers.js100%100%100%100%
   AwaitDependenciesInitFragment.js100%100%100%100%
   InferAsyncModulesPlugin.js100%100%100%100%
lib/cache
   AddBuildDependenciesPlugin.js100%100%100%100%
   AddManagedPathsPlugin.js100%100%100%100%
   IdleFileCachePlugin.js97.92%100%100%97.92%71, 83, 91
   MemoryCachePlugin.js95.83%100%100%95.83%33
   MemoryWithGcCachePlugin.js93.15%100%100%93.15%106, 113–114, 122, 89
   PackFileCacheStrategy.js96.40%100%100%96.40%1250, 1350, 1354, 1416, 628, 647, 657–659, 661, 677–678, 683, 686, 688, 693, 698, 722, 728, 762, 768, 774, 779, 790, 799, 804–805, 807, 824, 830–831, 833
   ResolverCachePlugin.js100%100%100%100%
   getLazyHashedEtag.js100%100%100%100%
   mergeEtags.js100%100%100%100%
lib/config
   browserslistTargetHandler.js100%100%100%100%
   defaults.js99.29%100%100%99.29%1411–1413, 1421, 271, 274, 279, 283
   normalization.js99%100%100%99%191–192, 258, 273
   target.js100%100%100%100%
lib/container
   ContainerEntryDependency.js100%100%100%100%
   ContainerEntryModule.js100%100%100%100%
   ContainerEntryModuleFactory.js100%100%100%100%
   ContainerExposedDependency.js100%100%100%100%
   ContainerPlugin.js100%100%100%100%
   ContainerReferencePlugin.js100%100%100%100%
   FallbackDependency.js100%100%100%100%
   FallbackItemDependency.js100%100%100%100%
   FallbackModule.js100%100%100%100%
   FallbackModuleFactory.js100%100%100%100%
   

@alexander-akait alexander-akait merged commit c8d97e2 into main May 20, 2026
54 of 61 checks passed
@alexander-akait alexander-akait deleted the claude/issue-20986-CIh26 branch May 20, 2026 20:15
alexander-akait added a commit that referenced this pull request May 21, 2026
…state

The previous iterative-only walker fixed the regression but still ran
~2x slower than the pre-#20993 recursive code on short-to-medium chains
because every descent went through explicit array-stack operations
instead of native call frames.

Restructure the walk as:

  walkSideEffectsRecursive(mod, depth)
    if depth >= SIDE_EFFECTS_RECURSION_LIMIT (2000):
      -> walkSideEffectsIterative(mod)   # stack-safe tail
    else:
      direct recursion, 1 native frame per module

The recursive form also folds the descent through
HarmonyImportSideEffectDependency.getModuleEvaluationSideEffectsState
directly into the loop, so each module costs one V8 stack frame instead
of two. That is why the limit can be set as high as 2000 without risking
the #20986 overflow (which triggered at ~1300 modules under the old
two-frame pattern).

Microbench (best of 7 runs, side-effect-free linear chain):

  chain   BEFORE    AFTER     NOW
   10     34.0 ms   190.8 ms  40.1 ms
   100    48.5 ms   191.3 ms  59.0 ms
   1000   60.3 ms   231.8 ms  69.8 ms
   20000  CRASH     111.5 ms  66.3 ms

Within 16-22% of the pre-regression recursive code on short / medium
chains, ~3x faster than the merged generator trampoline, and still
stack-safe at 20000 modules. A new unit test exercises the
recursive-to-iterative crossover with a bailout planted deep in the
iterative tail.
alexander-akait added a commit that referenced this pull request May 21, 2026
Builds on the previous hybrid walker by adding a per-module memoization
slot keyed by ModuleGraph. SideEffectsFlagPlugin queries
getSideEffectsConnectionState for every module while iterating
incoming connections and again inside its exportInfo.getTarget /
moveTarget callbacks, and the walker also re-encounters the same
sub-graphs as it descends through different starting modules. Without
the cache that work is O(N x avgDepth); with it the second and later
visits are O(1).

Design notes:

- Cache slot is a (ModuleGraph, ConnectionState) pair stored directly
  on the module (`_sideEffectsStateGraph` / `_sideEffectsStateValue`)
  rather than a WeakMap so the lookup is a single reference compare.
  When the next compilation queries with a different ModuleGraph the
  slot is overwritten on the first walk — no stale data leaks.
- Cycle safety: a result computed in the presence of a cycle can
  differ from the same module's fresh result (the back-edge target's
  contribution is hidden by CIRCULAR_CONNECTION short-circuiting), so
  we only memoize results whose walk subtree never observed a
  CIRCULAR_CONNECTION. A module-level `_sideEffectsCircularSeen` flag
  is save/restored at each recursive frame and at the public entry
  point so re-entrant calls (notably ConcatenatedModule.getSECS
  delegating to its rootModule) don't clobber the outer walk's
  tracking. A `true` result is monotonic — once any dep declares
  side effects, the answer is `true` regardless of cycle resolution —
  so it's always safe to memoize.
- Descents into non-NormalModule refModules conservatively mark the
  current frame as cycle-tainted, since we can't observe whether the
  inner public-method call hit a cycle back through the outer walk's
  stack.

Microbench (best of 7 runs):

  Repeated queries of every module in one chain — the realistic
  SideEffectsFlagPlugin access pattern:

    chain    BEFORE     AFTER (PR #20993)     NOW (cached)
     10      0.03 ms    n/a                   0.08 ms
     100     0.15 ms    n/a                   0.06 ms     2.5x
     1000   15.69 ms    n/a                   0.36 ms    44x
     5000  712.70 ms    n/a                   2.66 ms   268x

  First-walk-only (cold cache, fresh chain each call):

    chain    BEFORE     NOW (cached)
     10      333 ns     300 ns           (matches BEFORE)
     100     110 ns     131 ns
     1000    100 ns     148 ns
     5000    157 ns     526 ns           (iterative tail engaged)

All existing side-effect / tree-shaking / inner-graph / concatenate
ConfigTestCases pass (658 tests).
alexander-akait added a commit that referenced this pull request May 22, 2026
The previous linear-chain optimization only fired when a module had
exactly one entry in `dependencies` — but a real ESM module compiled
by webpack has the `HarmonyImportSideEffectDependency` *plus* an
`HarmonyExportSpecifierDependency` per `export` and a `ConstDependency`
per replaced expression. With the strict `length === 1` check the
1300-module cyclic chain (the canonical #20986 repro) fell through to
the general for-loop walk and overflowed V8's stack at ~1300 frames
again on Node 22+.

Loosen the linear-chain detection to "exactly one `SideEffectDep`
among any number of deps whose `getModuleEvaluationSideEffectsState`
returns `false`/`CIRCULAR_CONNECTION`". The walker now processes the
module's non-recursive deps in their declared order — bailing out
correctly with the first dep whose state is `true`, aggregating
non-`false` states into `nonRecursiveCurrent` — and only attempts the
linear tail-call when:

  - at most one `SideEffectDep` was seen,
  - no non-recursive dep triggered a bailout,
  - `nonRecursiveCurrent` stayed at `false` (otherwise the
    "ancestor's current = chain state" propagation rule no longer
    holds and we fall back to the general walk).

Verified on the actual #20986 repro (1300-module cyclic chain):

    AFTER  (#20993 generator trampoline)  ~12.3 s
    NOW    (this branch)                   ~6.5 s    (~1.9x faster)

  Larger chains (N >= 3000) hit pre-existing stack overflows in
  `ModuleConcatenationPlugin._tryToAdd` and
  `SideEffectsFlagPlugin.optimizeIncomingConnections` that are out of
  scope here — both branches behave identically there.

Adds a regression test that builds a 5000-module cyclic chain whose
modules have the same `[SideEffectDep, ConstDependency, ConstDependency]`
dependency shape as the real repro.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Getting getModuleEvaluationSideEffectsState(moduleGraph) error

2 participants