Skip to content

fix(treeshake): apply @__NO_SIDE_EFFECTS__ to cross-chunk namespace calls#9960

Merged
graphite-app[bot] merged 1 commit into
mainfrom
fix/9956-no-side-effects-namespace-cross-chunk
Jun 24, 2026
Merged

fix(treeshake): apply @__NO_SIDE_EFFECTS__ to cross-chunk namespace calls#9960
graphite-app[bot] merged 1 commit into
mainfrom
fix/9956-no-side-effects-namespace-cross-chunk

Conversation

@IWANABETHATGUY

Copy link
Copy Markdown
Member

Summary

@__NO_SIDE_EFFECTS__-annotated functions called through a namespace import (ns.fn()) were not tree-shaken when the annotated module landed in a separate/shared chunk, even though the same calls via named imports (fn()) and single-chunk namespace calls were eliminated correctly.

Fixes #9956.

Root cause

There are two independent DCE mechanisms for @__NO_SIDE_EFFECTS__:

  1. Link-stage cross_module_optimization — runs before chunking, resolves symbols cross-module, marks side-effect-free call statements for tree-shaking. But it only inspected it.callee.as_identifier(), so namespace-member callees (ns.fn, a StaticMemberExpression) were never recognized.
  2. Per-chunk dce-only minifier — reads the annotation directly, but only works when the annotated declaration and the call land in the same chunk.

So a namespace call across a chunk boundary fell through both: mechanism (1) skipped the member-expression callee, and mechanism (2) couldn't see the annotation living in the other chunk.

Fix

Two coordinated changes in cross_module_optimization.rs:

  1. New resolve_callee_canonical_symbol() unifies identifier callees with member-expression callees. For the latter it resolves ns.fn via the module's resolved_member_expr_refs, guarded by prop_and_related_span_list.is_empty() so ns.fn.bar() isn't misattributed to fn.
  2. The SideEffectDetector re-run previously passed None for namespace_object_symbol_ids. Even after marking the call pure, the detector recurses into the ns.fn callee read, which looks impure unless it knows ns is a namespace object. The set is reconstructed per-module from named_imports (the Specifier::Star entries — mirroring the scanner's add_star_import, which isn't persisted).

Both halves are required: marking the call pure alone leaves it kept because the callee read still reports a side effect.

Tests

  • no_side_effects_namespace_cross_chunk — minimal regression: two entries force shared.js into a shared chunk; both ns.fn() calls (and the chunk) are now eliminated.
  • no_side_effects_namespace_cross_chunk_cases — boundary matrix: computed access (ns['fn']()) and the function-expression form are eliminated; results that are used and ns.fn.call(...) (trailing property access) are preserved.

Both fixtures were verified to reproduce the bug on main (calls remain) and pass with the fix. Full Rust suite green: cargo test -p rolldown (1760 integration + 69 unit), clippy --deny warnings, fmt --check, and cargo check --all-features all clean.

🤖 Generated with Claude Code

@netlify

netlify Bot commented Jun 24, 2026

Copy link
Copy Markdown

Deploy Preview for rolldown-rs canceled.

Name Link
🔨 Latest commit 034f453
🔍 Latest deploy log https://app.netlify.com/projects/rolldown-rs/deploys/6a3bfd041609b300084d7463

@IWANABETHATGUY IWANABETHATGUY marked this pull request as draft June 24, 2026 13:58
@codspeed-hq

codspeed-hq Bot commented Jun 24, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 7 untouched benchmarks
⏩ 10 skipped benchmarks1


Comparing fix/9956-no-side-effects-namespace-cross-chunk (e0cd10a) with main (12d9647)

Open in CodSpeed

Footnotes

  1. 10 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.

@IWANABETHATGUY IWANABETHATGUY marked this pull request as ready for review June 24, 2026 15:05
@IWANABETHATGUY IWANABETHATGUY force-pushed the fix/9956-no-side-effects-namespace-cross-chunk branch from 2b840bb to e0cd10a Compare June 24, 2026 15:05

IWANABETHATGUY commented Jun 24, 2026

Copy link
Copy Markdown
Member Author

Merge activity

  • Jun 24, 3:50 PM UTC: The merge label 'graphite: merge-when-ready' was detected. This PR will be added to the Graphite merge queue once it meets the requirements.
  • Jun 24, 3:50 PM UTC: IWANABETHATGUY added this pull request to the Graphite merge queue.
  • Jun 24, 3:59 PM UTC: Merged by the Graphite merge queue.

…alls (#9960)

## Summary

`@__NO_SIDE_EFFECTS__`-annotated functions called through a namespace import (`ns.fn()`) were **not** tree-shaken when the annotated module landed in a separate/shared chunk, even though the same calls via named imports (`fn()`) and single-chunk namespace calls were eliminated correctly.

Fixes #9956.

## Root cause

There are two independent DCE mechanisms for `@__NO_SIDE_EFFECTS__`:

1. **Link-stage `cross_module_optimization`** — runs before chunking, resolves symbols cross-module, marks side-effect-free call statements for tree-shaking. But it only inspected `it.callee.as_identifier()`, so namespace-member callees (`ns.fn`, a `StaticMemberExpression`) were never recognized.
2. **Per-chunk `dce-only` minifier** — reads the annotation directly, but only works when the annotated declaration and the call land in the *same* chunk.

So a namespace call across a chunk boundary fell through both: mechanism (1) skipped the member-expression callee, and mechanism (2) couldn't see the annotation living in the other chunk.

## Fix

Two coordinated changes in `cross_module_optimization.rs`:

1. New `resolve_callee_canonical_symbol()` unifies identifier callees with member-expression callees. For the latter it resolves `ns.fn` via the module's `resolved_member_expr_refs`, guarded by `prop_and_related_span_list.is_empty()` so `ns.fn.bar()` isn't misattributed to `fn`.
2. The `SideEffectDetector` re-run previously passed `None` for `namespace_object_symbol_ids`. Even after marking the call pure, the detector recurses into the `ns.fn` callee *read*, which looks impure unless it knows `ns` is a namespace object. The set is reconstructed per-module from `named_imports` (the `Specifier::Star` entries — mirroring the scanner's `add_star_import`, which isn't persisted).

Both halves are required: marking the call pure alone leaves it kept because the callee read still reports a side effect.

## Tests

- `no_side_effects_namespace_cross_chunk` — minimal regression: two entries force `shared.js` into a shared chunk; both `ns.fn()` calls (and the chunk) are now eliminated.
- `no_side_effects_namespace_cross_chunk_cases` — boundary matrix: computed access (`ns['fn']()`) and the function-expression form are eliminated; results that are used and `ns.fn.call(...)` (trailing property access) are preserved.

Both fixtures were verified to reproduce the bug on `main` (calls remain) and pass with the fix. Full Rust suite green: `cargo test -p rolldown` (1760 integration + 69 unit), clippy `--deny warnings`, `fmt --check`, and `cargo check --all-features` all clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
@graphite-app graphite-app Bot force-pushed the fix/9956-no-side-effects-namespace-cross-chunk branch from e0cd10a to 034f453 Compare June 24, 2026 15:51
@graphite-app graphite-app Bot merged commit 034f453 into main Jun 24, 2026
30 checks passed
@graphite-app graphite-app Bot deleted the fix/9956-no-side-effects-namespace-cross-chunk branch June 24, 2026 15:59
graphite-app Bot pushed a commit that referenced this pull request Jun 24, 2026
## Summary

`SymbolRef::is_not_reassigned` returned `Option<bool>`, but it only ever produced `None` or `Some(true)` — it never returned `Some(false)`. Every caller already collapsed the `None` ("don't know") case to `false`:

- `cross_module_optimization.rs` (the namespace/identifier no-side-effects gate) and `impl_visit_mut.rs` (finalizer) used `== Some(true)`
- `render_chunk_exports.rs` used `.unwrap_or(false)`

So the `Option` carried no information the callers used. This changes the return type to a plain `bool` (unknown → conservatively `false`) and simplifies all three call sites.

No behavior change.

## Stacking

Stacked on top of #9960 (`fix/9956-...`). One of the updated call sites — the namespace-member gate in `cross_module_optimization.rs` — only exists on that branch, which is why this is stacked rather than branched off `main`.

## Verification

- `cargo test -p rolldown`: 1761 integration + 69 unit, 0 failed (no snapshot changes)
- `cargo clippy --workspace --all-targets -- --deny warnings`: clean
- `cargo check --all-features`: clean

🤖 Generated with [Claude Code](https://claude.com/claude-code)
@rolldown-guard rolldown-guard Bot mentioned this pull request Jul 1, 2026
shulaoda added a commit that referenced this pull request Jul 1, 2026
## [1.1.4] - 2026-07-01

### 🚀 Features

- disable `experimental.lazyBarrel` by default (#10071) by @shulaoda

### 🐛 Bug Fixes

- dev: disable lazy barrel in dev mode (#10060) by @shulaoda
- generate: keep full JSON interface under preserveModules namespa… (#10056) by @IWANABETHATGUY
- check finalize_other_specifiers in its own Debug attribute (#10032) by @shulaoda
- serialize the KeepAssign unused minify option as "keep_assign" (#10031) by @shulaoda
- keep fragments after the newline fragment in MagicString::last_line (#10023) by @shulaoda
- generate: undeclared JSON named exports under preserveModules (#10020) (#10027) by @IWANABETHATGUY
- deconflict: rename CJS-wrapped locals that shadow chunk-root bindings (#9921) by @IWANABETHATGUY
- rolldown: keep entry facade when a shared chunk holds another entry's module (#9997) by @hyf0
- treeshake: also bail JSON default split when the object escapes (#9996) by @IWANABETHATGUY
- don't classify await in a strict-mode function as top-level await (#9987) by @shulaoda
- avoid spurious leading newline in addon hooks (banner/footer/intro/outro) (#9989) by @shulaoda
- handle JSON default mutation bailouts (#9972) by @TheAlexLichter
- plugin: make lazy hook metadata enumerable (#9991) by @TheAlexLichter
- dev: make init errors in lazy-compiled modules catchable (#9981) by @h-a-n-a
- treeshake: keep computed-key side effects on namespace member access (#9986) by @shulaoda
- binding: validate replace plugin delimiters length instead of panicking (#9984) by @shulaoda
- reconstruct nested rest patterns in into_expression (#9980) by @IWANABETHATGUY
- reconstruct rest patterns as spread in into_expression (#9976) by @shulaoda
- preserve export keyword on multi-declarator exports under keepNames (#9974) by @shulaoda
- deterministically keep the shortest name for deduplicated assets (#9948) by @x1024
- treeshake: apply @__NO_SIDE_EFFECTS__ to cross-chunk namespace calls (#9960) by @IWANABETHATGUY

### 🚜 Refactor

- drop redundant program scope enter/leave in finalizer (#10049) by @shulaoda
- deconflict: extract collect_chunk_scope_captured_names (#10006) by @IWANABETHATGUY
- unify pre-scan multi-declarator split into one decision site (#9982) by @IWANABETHATGUY
- common: return bool from SymbolRef::is_not_reassigned (#9962) by @IWANABETHATGUY

### 📚 Documentation

- rolldown: remove outdated comment for removing parenthesized expression (#10062) by @Dunqing
- use GitHub-flavored alert for Etiquette note in contribution guide (#10012) by @IWANABETHATGUY
- replace: explain the delimiters left and right boundaries (#9985) by @shulaoda
- ast-mutation: remove stale Address Use section after pre-scan refactor (#9983) by @IWANABETHATGUY
- remove fathom (#9968) by @mdong1909
- contribution-guide: code-format main branch references (#9966) by @IWANABETHATGUY
- contribution-guide: fix stale REPL note and tidy wording (#9957) by @hyf0
- contribution-guide: clarify when to discuss before opening a PR (#9955) by @hyf0

### ⚡ Performance

- disable preserve_parens across all parse paths (#10057) by @Dunqing
- common: inline declared_symbols with SmallVec (#9920) by @IWANABETHATGUY
- common: pack TaggedSymbolRef into 8 bytes (#9919) by @IWANABETHATGUY
- sourcemap: skip newline scan on the no-sourcemap join fast path (#9936) by @Boshen

### 🧪 Testing

- dev: error in lazy module should be catchable (#9975) by @sapphi-red
- dev: reject unknown lazy compile modules (#9969) by @sapphi-red

### ⚙️ Miscellaneous Tasks

- deps: update actions/cache action to v6 (#10001) by @renovate[bot]
- trigger vite ecosystem-ci from PR comments (#10058) by @shulaoda
- deps: update napi to v3.10.0 (#10063) by @renovate[bot]
- remove unused From impl for RolldownLabelSpan (#10055) by @shulaoda
- remove dead Diagnostic::with_kind method (#10054) by @shulaoda
- remove unused StatementExt methods (#10053) by @shulaoda
- remove unused ExpressionExt methods (#10052) by @shulaoda
- remove commented-out re_export_all_names field (#10051) by @shulaoda
- deps: update pnpm to v11.9.0 (#10047) by @renovate[bot]
- remove the unused BindingGenerateHmrPatchReturn napi type (#10034) by @shulaoda
- remove the dead inline_entry_chunk_wrapping scaffolding (#10037) by @shulaoda
- deps: bump oxc_resolver to 11.22.0 (#10045) by @Boshen
- remove never-constructed MatchImportKind::_Ignore variant (#10041) by @shulaoda
- remove the unused ScheduledBuild napi struct (#10033) by @shulaoda
- remove dead compute_hmr_update_single method (#10040) by @shulaoda
- drop the redundant visited.insert in manual code splitting (#10038) by @shulaoda
- remove the dead output_assets vector in render_chunk_to_assets (#10036) by @shulaoda
- remove the unused From<String>/Display impls for BindingLogLevel (#10035) by @shulaoda
- deps: upgrade oxc to 0.138.0 and migrate to per-type AST construction (#10018) by @shulaoda
- deps: update rust crates (#9911) by @renovate[bot]
- deps: update test262 submodule for tests (#10016) by @rolldown-guard[bot]
- deps: update github actions (#9999) by @renovate[bot]
- deps: update npm packages (#10000) by @renovate[bot]

### ◀️ Revert

- "fix(plugin): make lazy hook metadata enumerable (#9991)" (#10005) by @shulaoda

### ❤️ New Contributors

* @x1024 made their first contribution in [#9948](#9948)

Co-authored-by: shulaoda <165626830+shulaoda@users.noreply.github.com>
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.

[Bug]: NO_SIDE_EFFECTS annotation does not work for namespace import calls across chunk boundaries

2 participants