Skip to content

side-effect detector loses PureAnnotation flag when a /* @__PURE__ */ call is nested inside ObjectExpression / ArrayExpression #9425

@shulaoda

Description

@shulaoda

Description

Summary

SideEffectDetector in crates/rolldown/src/ast_scanner/side_effect_detector/mod.rs only sets SideEffectDetail::PureAnnotation for a CallExpression that is itself the top-level expression being inspected. When the pure-annotated call is nested inside a compound expression (object literal property value, array element, sequence, etc.) the detector falls through to oxc's MayHaveSideEffects, which honors the pure flag and reports false, and the PureAnnotation flag never propagates up to the statement.

Concrete fall-through in detect_side_effect_of_expr:

// crates/rolldown/src/ast_scanner/side_effect_detector/mod.rs
match expr {
    // ... explicit handlers for Member, Identifier, Assignment, Chain, Update, New, Call ...
    _ => expr.may_have_side_effects(self).into(),  // ObjectExpression hits this
}

Because the statement-level side_effect doesn't intersect PureAnnotation / GlobalVarAccess / Unknown, EcmaViewMeta::ExecutionOrderSensitive is not set, and wrap_modules (crates/rolldown/src/stages/link_stage/wrapping.rs) takes the avoid_wrapping branch:

let avoid_wrapping = on_demand_wrapping
  && !module.meta.contains(EcmaViewMeta::ExecutionOrderSensitive)
  && module.import_records.is_empty()
  && !linking_info.required_by_other_module;

The module is given WrapKind::None and its top-level var initializer is emitted at chunk top level, before any setup.js-style sibling module runs.

Reproduction

Minimal source — already present as two integration tests:

  • crates/rolldown/tests/rolldown/function/experimental/strict_execution_order/concatenate_wrapped_modules
  • crates/rolldown/tests/rolldown/function/experimental/strict_execution_order/issue_5303
// foo_inner.js
export default {
  foo: /* #__PURE__ */ (() => globalValue)(),
};

// setup.js — runs before foo_inner.js per strictExecutionOrder
globalThis.globalValue = 'foo';

Expected bundled output (current snapshots): foo_inner.js is wrapped in __esmMin(...) so the assignment happens after setup.js.

Actual bundled output after bumping oxc to 0.131:

import assert from "node:assert";
//#region \0rolldown/runtime.js
var __esmMin = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
//#endregion
//#region foo_inner.js
var foo_inner_default = { foo: /* @__PURE__ */ (() => globalValue)() };
__esmMin((() => {
  //#region setup.js
  globalThis.globalValue = "foo";
  //#endregion
  //#region foo.js
  assert.strictEqual(foo_inner_default.foo, "foo");
  //#endregion
  //#region main.js
  assert.equal(foo_inner_default.foo, "foo");
  //#endregion
}))();

var foo_inner_default = ... runs at top level, before globalThis.globalValue is assigned, producing:

ReferenceError: globalValue is not defined

Why this was latent before oxc 0.131

oxc 0.130's DCE pass unconditionally inlined (/* @__PURE__ */ (() => globalValue)()) down to a bare globalValue identifier. After inlining, rolldown's GlobalVarAccess detection path (an independent code path from PureAnnotation) caught the unresolved global and marked the module ExecutionOrderSensitive, so it got wrapped. The wrap-correctness was accidentally relying on oxc inlining the IIFE — the detector's nested-PURE handling has been incomplete this whole time.

oxc 0.131 PR oxc-project/oxc#22349 added iife_inline_would_lose_pure: in DCE-only mode the minifier now intentionally preserves a pure-annotated IIFE whose body has side effects, so the annotation survives for downstream tree-shakers. That is the correct call on oxc's side — but it removes the inline-into-bare-global behavior that rolldown was implicitly leaning on, surfacing this detector gap.

Suggested fix

Stop delegating compound expressions to oxc's MayHaveSideEffects and recurse through rolldown's own detector instead, so flags like PureAnnotation and GlobalVarAccess propagate to the statement level. At minimum, handle ObjectExpression, ArrayExpression, and SequenceExpression in detect_side_effect_of_expr with explicit arms that fold their children's SideEffectDetail together. The existing detect_side_effect_of_call_expr already produces the right flags for a leaf pure call; the missing piece is propagation through the surrounding compound expression.

Related

Metadata

Metadata

Assignees

Type

No type

Priority

None yet

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions