Skip to content

semantic: extend no_side_effects to include empty functions #20290

@Dunqing

Description

@Dunqing

Summary

Currently, Scoping::no_side_effects only tracks symbols annotated with @__NO_SIDE_EFFECTS__ (the .pure flag).

// Save `@__NO_SIDE_EFFECTS__` for function initializers.
if let BindingPattern::BindingIdentifier(id) = &self.id
&& let Some(symbol_id) = id.symbol_id.get()
&& let Some(init) = &self.init
&& match init {
Expression::FunctionExpression(func) => func.pure,
Expression::ArrowFunctionExpression(func) => func.pure,
_ => false,
}
{
builder.scoping.no_side_effects.insert(symbol_id);
}

Propose also marking functions with empty bodies and simple parameters (no destructuring, no defaults).

This would allow Rolldown to use scoping.no_side_effects() as the single source of truth for "calling this function has no side effects", eliminating the need for a separate SymbolRefFlags::SideEffectsFreeFunction flag that currently duplicates this logic in the bundler.

Current Rolldown duplication

Rolldown maintains its own empty-function detection in rolldown_ecmascript_utils/src/extensions/ast_ext/function.rs and uses it alongside .pure in the scanner (ast_scanner/mod.rs#L738-L748):

let is_side_effect_free_function = decl
  .init
  .as_ref()
  .map(|expr| match expr {
    Expression::FunctionExpression(func) => func.is_side_effect_free() || func.pure,
    Expression::ArrowFunctionExpression(func) => {
      func.is_side_effect_free() || func.pure
    }
    _ => false,
  })
  .unwrap_or(false);

This sets SymbolRefFlags::SideEffectsFreeFunction, which is then consumed during cross-module optimization (cross_module_optimization.rs#L62-L85) to identify pure function calls across module boundaries.

Proposed change

In crates/oxc_semantic/src/binder.rs:

Function::bind (line ~155):

// Current
if self.pure {
    builder.scoping.no_side_effects.insert(symbol_id);
}

// Proposed
if self.pure || is_empty_and_simple_params(self) {
    builder.scoping.no_side_effects.insert(symbol_id);
}

VariableDeclarator::bind (line ~101): same extension for function expression / arrow function initializers.

Where "empty and simple params" means:

  • Body has no statements (or None)
  • All params are BindingIdentifier with no initializer

Industry precedent

Tool Removes calls to empty functions? Mechanism
Rollup Yes (tree-shaking) Traces into function body via hasEffectsOnInteractionAtPath
esbuild Yes (minify) IsEmptyFunction symbol flag + removal at print time
Terser Yes (with toplevel) Inlines empty function body, then drops
SWC No Conservative, requires explicit annotations

3 out of 4 major tools treat empty function calls as side-effect-free. Verified with actual test runs against Rollup 4.59 and esbuild 0.27.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions