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
Description
Summary
SideEffectDetectorincrates/rolldown/src/ast_scanner/side_effect_detector/mod.rsonly setsSideEffectDetail::PureAnnotationfor aCallExpressionthat 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'sMayHaveSideEffects, which honors thepureflag and reportsfalse, and thePureAnnotationflag never propagates up to the statement.Concrete fall-through in
detect_side_effect_of_expr:Because the statement-level
side_effectdoesn't intersectPureAnnotation/GlobalVarAccess/Unknown,EcmaViewMeta::ExecutionOrderSensitiveis not set, andwrap_modules(crates/rolldown/src/stages/link_stage/wrapping.rs) takes theavoid_wrappingbranch:The module is given
WrapKind::Noneand its top-levelvarinitializer is emitted at chunk top level, before anysetup.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_modulescrates/rolldown/tests/rolldown/function/experimental/strict_execution_order/issue_5303Expected bundled output (current snapshots):
foo_inner.jsis wrapped in__esmMin(...)so the assignment happens aftersetup.js.Actual bundled output after bumping oxc to 0.131:
var foo_inner_default = ...runs at top level, beforeglobalThis.globalValueis assigned, producing:Why this was latent before oxc 0.131
oxc 0.130's DCE pass unconditionally inlined
(/* @__PURE__ */ (() => globalValue)())down to a bareglobalValueidentifier. After inlining, rolldown'sGlobalVarAccessdetection path (an independent code path fromPureAnnotation) caught the unresolved global and marked the moduleExecutionOrderSensitive, 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
MayHaveSideEffectsand recurse through rolldown's own detector instead, so flags likePureAnnotationandGlobalVarAccesspropagate to the statement level. At minimum, handleObjectExpression,ArrayExpression, andSequenceExpressionindetect_side_effect_of_exprwith explicit arms that fold their children'sSideEffectDetailtogether. The existingdetect_side_effect_of_call_expralready produces the right flags for a leaf pure call; the missing piece is propagation through the surrounding compound expression.Related