With treeshake.manualPureFunctions, a call to a listed function that sits inside a member or new chain is deleted together with subexpressions that JavaScript evaluates eagerly — call arguments, computed property keys, and the new callee chain. Their side effects are silently dropped. A bare pure call (make(effect())) is handled correctly; the bug is specific to chains.
Repro
rolldown 1.2.0 (latest release); the responsible short-circuit is unchanged on current main (4930a00).
// main.mjs
import { make } from 'ext';
function effect() {
console.log('effect');
return 'k';
}
make()[effect()];
make(effect()).value;
new (make()[effect()].Box)();
// rolldown.config.mjs
export default {
input: 'main.mjs',
external: ['ext'],
treeshake: { manualPureFunctions: ['make'] },
output: { file: 'out.mjs', format: 'esm' },
};
Actual output — the whole module body is gone, including the three effect() calls:
Expected: deleting the pure calls themselves is fine, but the eagerly-evaluated effect() subexpressions must be kept. Each isolated statement reproduces on its own.
Controls:
- Without
manualPureFunctions, all three statements are retained.
- A bare call is already handled correctly:
make(effect()); compiles to effect();.
Comparison with Rollup
Rollup 4.62.2 with the identical input and config retains all three statements. Rollup's documentation promises this in the manualPureFunctions warning:
If you pass arguments to such a pure function, those arguments are still checked for direct side effects like mutating a variable or calling a global function, in which case the call to the pure function is retained.
Cause
analyze_member_expr short-circuits to "no side effects" when the member chain's root resolves to a manual pure function, before analyzing anything inside the chain: stmt_eval_analyzer/mod.rs#L192-L194. The namespace branch directly below shows the intended pattern — its comment notes that a computed key is still evaluated and may have its own side effects, and it recurses into ComputedMemberExpression keys (#L195-L203).
- The pure-function match is by chain root only:
extract_first_part_of_member_expr_like walks through computed members, static members, and calls down to the leftmost identifier (#L832-L863).
- The same root-match is exposed to Oxc via the
MayHaveSideEffectsContext::manual_pure_functions impl (#L880-L882), which is how the new (make()[effect()].Box)() callee-chain effects get dropped.
Provenance
Found by the execution-order fuzzer while auditing manually-pure calls; reduced cases and current-target signatures are recorded in rolldown-order-fuzzer@77f196b. It reproduces with fully default options and is an ordinary tree-shaking bug, unrelated to strictExecutionOrder or wrapping. Extracted from the fuzzer-findings list in #10294 (comment).
With
treeshake.manualPureFunctions, a call to a listed function that sits inside a member ornewchain is deleted together with subexpressions that JavaScript evaluates eagerly — call arguments, computed property keys, and thenewcallee chain. Their side effects are silently dropped. A bare pure call (make(effect())) is handled correctly; the bug is specific to chains.Repro
rolldown 1.2.0 (latest release); the responsible short-circuit is unchanged on current
main(4930a00).Actual output — the whole module body is gone, including the three
effect()calls:Expected: deleting the pure calls themselves is fine, but the eagerly-evaluated
effect()subexpressions must be kept. Each isolated statement reproduces on its own.Controls:
manualPureFunctions, all three statements are retained.make(effect());compiles toeffect();.Comparison with Rollup
Rollup 4.62.2 with the identical input and config retains all three statements. Rollup's documentation promises this in the
manualPureFunctionswarning:Cause
analyze_member_exprshort-circuits to "no side effects" when the member chain's root resolves to a manual pure function, before analyzing anything inside the chain: stmt_eval_analyzer/mod.rs#L192-L194. The namespace branch directly below shows the intended pattern — its comment notes that a computed key is still evaluated and may have its own side effects, and it recurses intoComputedMemberExpressionkeys (#L195-L203).extract_first_part_of_member_expr_likewalks through computed members, static members, and calls down to the leftmost identifier (#L832-L863).MayHaveSideEffectsContext::manual_pure_functionsimpl (#L880-L882), which is how thenew (make()[effect()].Box)()callee-chain effects get dropped.Provenance
Found by the execution-order fuzzer while auditing manually-pure calls; reduced cases and current-target signatures are recorded in rolldown-order-fuzzer@77f196b. It reproduces with fully default options and is an ordinary tree-shaking bug, unrelated to
strictExecutionOrderor wrapping. Extracted from the fuzzer-findings list in #10294 (comment).