Skip to content

feat(rolldown): inline optional-chain enum access#9379

Merged
Dunqing merged 1 commit into
mainfrom
feat/inline-optional-enum-access
May 13, 2026
Merged

feat(rolldown): inline optional-chain enum access#9379
Dunqing merged 1 commit into
mainfrom
feat/inline-optional-enum-access

Conversation

@Dunqing

@Dunqing Dunqing commented May 12, 2026

Copy link
Copy Markdown
Contributor

We already implemented single file optional chaining inline in Oxc oxc-project/oxc#21834

Summary

Teaches the cross-module enum inliner to fold optional-chain access (E?.x, E?.['x']) to the same literal as E.x / E['x']. With that hole filled, the tree-shaker bypass added in #9229 no longer needs its !prop.optional guard.

Why this is safe

Enum bindings are always defined — the generated IIFE initializes them to {} (never null/undefined), and const enums are removed entirely after inlining. So ?. cannot short-circuit on an enum object: E?.x is observationally equivalent to E.x.

The TDZ assumption this relies on (an enum identifier reaching the finalizer is always bound to a real value) is the same one E.x inlining already makes — we just extend it to the optional form.

What changed

  • crates/rolldown/src/module_finalizers/mod.rstry_inline_enum_access now also matches ChainExpression { StaticMemberExpression | ComputedMemberExpression }.
  • crates/rolldown/src/module_finalizers/impl_visit_mut.rs — pre-match inline attempt for ChainExpression, done before the main visit_expression dispatch so expr can be passed to try_inline_enum_access without conflicting with the existing arm's mutable destructure of the inner chain.
  • crates/rolldown/src/stages/link_stage/tree_shaking/include_statements.rs — drops the !prop.optional guard from the enum-decl bypass. !is_write stays.
  • crates/rolldown/tests/esbuild/ts/ts_enum_cross_module_inlining_access/ — moves the optional cases into the inlined group; snapshot now folds all 8 inlinable accesses to literals while keeping e_num/e_str (bare identifier references).

Test plan

  • cargo test -p rolldown --test integration ts_enum_cross_module_inlining_access
  • cargo test -p rolldown (1703 passed, 0 failed)
  • cargo test -p rolldown --test integration enum (24 passed)
  • cargo test -p rolldown --test integration chain (25 passed)
  • Spot-checked single-module enum E { x = 1 }; console.log(E?.x) via CLI — now folds to console.log(1)
  • just ued — produced no additional snap-diff changes

🤖 Generated with Claude Code

@netlify

netlify Bot commented May 12, 2026

Copy link
Copy Markdown

Deploy Preview for rolldown-rs ready!

Name Link
🔨 Latest commit 89201fe
🔍 Latest deploy log https://app.netlify.com/projects/rolldown-rs/deploys/6a04462ea077dd00084076be
😎 Deploy Preview https://deploy-preview-9379--rolldown-rs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@codspeed-hq

codspeed-hq Bot commented May 12, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 4 untouched benchmarks
⏩ 10 skipped benchmarks1


Comparing feat/inline-optional-enum-access (89201fe) with main (83addfe)2

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.

  2. No successful run was found on main (6fb4811) during the generation of this report, so 83addfe was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@Dunqing Dunqing force-pushed the feat/inline-optional-enum-access branch from a67b701 to 99b875f Compare May 13, 2026 01:36
@Dunqing Dunqing enabled auto-merge (squash) May 13, 2026 09:36
`try_inline_enum_access` only matched `StaticMemberExpression` /
`ComputedMemberExpression`. Optional-chain enum accesses (`E?.x`,
`E?.['x']`) wrap their inner member in a `ChainExpression`, so the
finalizer never inlined them — the access survived in the output as a
runtime member lookup.

Extend the matcher to also walk through `ChainExpression`, treating
`E?.x` as equivalent to `E.x`: enum bindings are always defined (the
IIFE initializes them to `{}` — never null/undefined), so `?.` cannot
short-circuit. Done before the main `visit_expression` dispatch so we
can pass `expr` to `try_inline_enum_access` without conflicting with
the `ChainExpression` arm's mutable destructure of the inner chain.

With the finalizer now able to inline optional access, drop the
`!prop.optional` guard from the tree-shaker bypass in
`include_statements.rs` (added in #9229 to keep the declaration alive
while the access wasn't inlinable). The `!is_write` guard stays —
writes still aren't inlined.

The `ts_enum_cross_module_inlining_access` fixture moves the optional
cases into the `inlined` group, and the snapshot now folds all 8
inlinable accesses to their literals while preserving the `e_num` /
`e_str` declarations referenced as bare identifiers.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
@Dunqing Dunqing force-pushed the feat/inline-optional-enum-access branch from 99b875f to 89201fe Compare May 13, 2026 09:36
@Dunqing Dunqing merged commit debe998 into main May 13, 2026
31 checks passed
@Dunqing Dunqing deleted the feat/inline-optional-enum-access branch May 13, 2026 09:40
@rolldown-guard rolldown-guard Bot mentioned this pull request May 13, 2026
shulaoda added a commit that referenced this pull request May 13, 2026
## [1.0.1] - 2026-05-13

### 🚀 Features

- experimental/lazy-barrel: advice on oversized barrel modules (#9236) by @shulaoda
- rolldown: inline optional-chain enum access (#9379) by @Dunqing
- chunk-optimization: dedupe already-loaded dynamic deps (#9305) by @IWANABETHATGUY
- binding: call moduleParsed hook in ParallelJsPlugin (#9318) by @jaehafe

### 🐛 Bug Fixes

- transform: enable `enum_eval` for `transformSync` and vite TS transform (#9325) by @Dunqing
- error: remove severity prefix from diagnostic messages (#9262) by @Kyujenius
- deps: pin pnpm to 10.23.0 to work around catalog mismatch on Netlify (#9364) by @shulaoda
- ci: pin mimalloc-safe to 0.1.58 (#9361) by @shulaoda
- dev/lazy: fix exports of lazy requests in lazy chunks (#9249) by @h-a-n-a
- rolldown_plugin_vite_resolve: handle errors in `resolveSubpathImports` callback (#9355) by @sapphi-red
- rolldown_plugin_lazy_compilation: use loadExports for fetched proxy to preserve original export names (#9132) by @h-a-n-a
- common: include offending index in HybridIndexVec panic message (#9296) by @SAY-5

### 🚜 Refactor

- ecmascript: extract semantic_builder_for_transform helper (#9326) by @Dunqing
- test: extract reusable static-import-cycle helper (#9332) by @IWANABETHATGUY

### 📚 Documentation

- clarify scope of `topLevelVar` (#9380) by @IWANABETHATGUY
- meta/design: add ast-mutation design doc (#9338) by @hyf0
- feat: add ai policy in contribution guide (#9315) by @mdong1909

### ⚡ Performance

- binding: enable mimalloc v3 to reduce idle memory (#9349) by @shulaoda

### 🧪 Testing

- mcs: cover require() in `$initial` group (#9376) by @hyf0
- add regression for CJS facade chunk merge into entry (#9351) by @IWANABETHATGUY

### ⚙️ Miscellaneous Tasks

- switch prepare-release to manual dispatch with version input (#9383) by @shulaoda
- migrate `@rolldown/pluginutils` to `rolldown/plugins` (#9317) by @shulaoda
- deps: pin libmimalloc-sys2 to 0.1.54 (#9372) by @shulaoda
- replace `igorskyflyer/action-readfile` with `cat` (#9369) by @sapphi-red
- deps: update test262 submodule for tests (#9371) by @rolldown-guard[bot]
- use app token for test dep update PRs (#9368) by @sapphi-red
- replace some actions with gh commands (#9367) by @sapphi-red
- replace action-semantic-pull-request with inline regex (#9366) by @sapphi-red
- remove pull_request_target workflows (#9188) by @Boshen
- deps: upgrade oxc to 0.130.0 (#9360) by @shulaoda
- deps: update github actions (major) (#9348) by @renovate[bot]
- deps: update github actions (#9341) by @renovate[bot]
- deps: update rust crates (#9344) by @renovate[bot]
- deps: update crate-ci/typos action to v1.46.1 (#9357) by @renovate[bot]
- deps: update npm packages (#9343) by @renovate[bot]
- deps: update pnpm to v10.33.4 (#9347) by @renovate[bot]
- deps: update dependency rolldown-plugin-dts to ^0.25.0 (#9346) by @renovate[bot]
- .claude: add rolldown-repl encoder, rename decode skill (#9352) by @IWANABETHATGUY
- deps: update crate-ci/typos action to v1.46.0 (#9345) by @renovate[bot]
- deps: update napi to v3.8.6 (#9342) by @renovate[bot]
- deps: update dependency vite-plus to v0.1.20 (#9340) by @renovate[bot]
- enable rollup chunking-form test (#9335) by @IWANABETHATGUY
- typo: fix typo in watcher options comment (#9324) by @thescripted

### ❤️ New Contributors

* @Kyujenius made their first contribution in [#9262](#9262)
* @SAY-5 made their first contribution in [#9296](#9296)
* @thescripted made their first contribution in [#9324](#9324)

Co-authored-by: shulaoda <[email protected]>
IWANABETHATGUY pushed a commit that referenced this pull request May 18, 2026
We already implemented single file optional chaining inline in Oxc
oxc-project/oxc#21834

## Summary

Teaches the cross-module enum inliner to fold optional-chain access
(`E?.x`, `E?.['x']`) to the same literal as `E.x` / `E['x']`. With that
hole filled, the tree-shaker bypass added in #9229 no longer needs its
`!prop.optional` guard.

## Why this is safe

Enum bindings are always defined — the generated IIFE initializes them
to `{}` (never null/undefined), and const enums are removed entirely
after inlining. So `?.` cannot short-circuit on an enum object: `E?.x`
is observationally equivalent to `E.x`.

The TDZ assumption this relies on (an enum identifier reaching the
finalizer is always bound to a real value) is the same one `E.x`
inlining already makes — we just extend it to the optional form.

## What changed

- `crates/rolldown/src/module_finalizers/mod.rs` —
`try_inline_enum_access` now also matches `ChainExpression {
StaticMemberExpression | ComputedMemberExpression }`.
- `crates/rolldown/src/module_finalizers/impl_visit_mut.rs` — pre-match
inline attempt for `ChainExpression`, done before the main
`visit_expression` dispatch so `expr` can be passed to
`try_inline_enum_access` without conflicting with the existing arm's
mutable destructure of the inner chain.
-
`crates/rolldown/src/stages/link_stage/tree_shaking/include_statements.rs`
— drops the `!prop.optional` guard from the enum-decl bypass.
`!is_write` stays.
-
`crates/rolldown/tests/esbuild/ts/ts_enum_cross_module_inlining_access/`
— moves the optional cases into the `inlined` group; snapshot now folds
all 8 inlinable accesses to literals while keeping `e_num`/`e_str` (bare
identifier references).

## Test plan

- [x] `cargo test -p rolldown --test integration
ts_enum_cross_module_inlining_access`
- [x] `cargo test -p rolldown` (1703 passed, 0 failed)
- [x] `cargo test -p rolldown --test integration enum` (24 passed)
- [x] `cargo test -p rolldown --test integration chain` (25 passed)
- [x] Spot-checked single-module `enum E { x = 1 }; console.log(E?.x)`
via CLI — now folds to `console.log(1)`
- [x] `just ued` — produced no additional snap-diff changes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 <[email protected]>
IWANABETHATGUY pushed a commit that referenced this pull request May 18, 2026
## [1.0.1] - 2026-05-13

### 🚀 Features

- experimental/lazy-barrel: advice on oversized barrel modules (#9236) by @shulaoda
- rolldown: inline optional-chain enum access (#9379) by @Dunqing
- chunk-optimization: dedupe already-loaded dynamic deps (#9305) by @IWANABETHATGUY
- binding: call moduleParsed hook in ParallelJsPlugin (#9318) by @jaehafe

### 🐛 Bug Fixes

- transform: enable `enum_eval` for `transformSync` and vite TS transform (#9325) by @Dunqing
- error: remove severity prefix from diagnostic messages (#9262) by @Kyujenius
- deps: pin pnpm to 10.23.0 to work around catalog mismatch on Netlify (#9364) by @shulaoda
- ci: pin mimalloc-safe to 0.1.58 (#9361) by @shulaoda
- dev/lazy: fix exports of lazy requests in lazy chunks (#9249) by @h-a-n-a
- rolldown_plugin_vite_resolve: handle errors in `resolveSubpathImports` callback (#9355) by @sapphi-red
- rolldown_plugin_lazy_compilation: use loadExports for fetched proxy to preserve original export names (#9132) by @h-a-n-a
- common: include offending index in HybridIndexVec panic message (#9296) by @SAY-5

### 🚜 Refactor

- ecmascript: extract semantic_builder_for_transform helper (#9326) by @Dunqing
- test: extract reusable static-import-cycle helper (#9332) by @IWANABETHATGUY

### 📚 Documentation

- clarify scope of `topLevelVar` (#9380) by @IWANABETHATGUY
- meta/design: add ast-mutation design doc (#9338) by @hyf0
- feat: add ai policy in contribution guide (#9315) by @mdong1909

### ⚡ Performance

- binding: enable mimalloc v3 to reduce idle memory (#9349) by @shulaoda

### 🧪 Testing

- mcs: cover require() in `$initial` group (#9376) by @hyf0
- add regression for CJS facade chunk merge into entry (#9351) by @IWANABETHATGUY

### ⚙️ Miscellaneous Tasks

- switch prepare-release to manual dispatch with version input (#9383) by @shulaoda
- migrate `@rolldown/pluginutils` to `rolldown/plugins` (#9317) by @shulaoda
- deps: pin libmimalloc-sys2 to 0.1.54 (#9372) by @shulaoda
- replace `igorskyflyer/action-readfile` with `cat` (#9369) by @sapphi-red
- deps: update test262 submodule for tests (#9371) by @rolldown-guard[bot]
- use app token for test dep update PRs (#9368) by @sapphi-red
- replace some actions with gh commands (#9367) by @sapphi-red
- replace action-semantic-pull-request with inline regex (#9366) by @sapphi-red
- remove pull_request_target workflows (#9188) by @Boshen
- deps: upgrade oxc to 0.130.0 (#9360) by @shulaoda
- deps: update github actions (major) (#9348) by @renovate[bot]
- deps: update github actions (#9341) by @renovate[bot]
- deps: update rust crates (#9344) by @renovate[bot]
- deps: update crate-ci/typos action to v1.46.1 (#9357) by @renovate[bot]
- deps: update npm packages (#9343) by @renovate[bot]
- deps: update pnpm to v10.33.4 (#9347) by @renovate[bot]
- deps: update dependency rolldown-plugin-dts to ^0.25.0 (#9346) by @renovate[bot]
- .claude: add rolldown-repl encoder, rename decode skill (#9352) by @IWANABETHATGUY
- deps: update crate-ci/typos action to v1.46.0 (#9345) by @renovate[bot]
- deps: update napi to v3.8.6 (#9342) by @renovate[bot]
- deps: update dependency vite-plus to v0.1.20 (#9340) by @renovate[bot]
- enable rollup chunking-form test (#9335) by @IWANABETHATGUY
- typo: fix typo in watcher options comment (#9324) by @thescripted

### ❤️ New Contributors

* @Kyujenius made their first contribution in [#9262](#9262)
* @SAY-5 made their first contribution in [#9296](#9296)
* @thescripted made their first contribution in [#9324](#9324)

Co-authored-by: shulaoda <[email protected]>
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.

2 participants