diff --git a/crates/oxc_minifier/src/peephole/inline.rs b/crates/oxc_minifier/src/peephole/inline.rs index bdcc3a760b..1509945aa7 100644 --- a/crates/oxc_minifier/src/peephole/inline.rs +++ b/crates/oxc_minifier/src/peephole/inline.rs @@ -10,6 +10,52 @@ use crate::symbol_value::FreshValueKind; use super::PeepholeOptimizations; impl<'a> PeepholeOptimizations { + /// Seed parameter constants before traversing an immediately invoked function. + /// The arguments stay at the call site, preserving their evaluation order. + pub fn init_iife_parameter_values(call: &CallExpression<'a>, ctx: &mut TraverseCtx<'a>) { + let params = match &call.callee { + Expression::ArrowFunctionExpression(arrow) => &arrow.params, + Expression::FunctionExpression(function) => { + // Sloppy functions expose mapped arguments and caller introspection. + // A referenced function name can escape or recurse with other arguments. + if !ctx.scoping().scope_flags(function.scope_id()).is_strict_mode() + || function + .id + .as_ref() + .is_some_and(|id| !ctx.scoping().symbol_is_unused(id.symbol_id())) + { + return; + } + &function.params + } + _ => return, + }; + if params.items.is_empty() + || params.rest.is_some() + || params + .items + .iter() + .any(|param| !param.pattern.is_binding_identifier() || param.initializer.is_some()) + || call.arguments.iter().any(Argument::is_spread) + { + return; + } + for (param, argument) in params.items.iter().zip(&call.arguments) { + let BindingPattern::BindingIdentifier(id) = ¶m.pattern else { unreachable!() }; + let symbol_id = id.symbol_id(); + // A body declaration can replace a parameter before its first read. + if !ctx.scoping().symbol_redeclarations(symbol_id).is_empty() { + continue; + } + if let Some(value) = argument.to_expression().evaluate_value(ctx) { + // `init_value` and its consumers reject direct eval and writes, + // including writes from nested closures. + ctx.init_value(symbol_id, Some(value), FreshValueKind::None, false, false) + .allow_constant_inlining = false; + } + } + } + pub fn init_symbol_value(decl: &VariableDeclarator<'a>, ctx: &mut TraverseCtx<'a>) { let Ancestor::VariableDeclarationDeclarations(declaration) = ctx.parent() else { unreachable!(); diff --git a/crates/oxc_minifier/src/peephole/mod.rs b/crates/oxc_minifier/src/peephole/mod.rs index 978014fc7e..b24e936489 100644 --- a/crates/oxc_minifier/src/peephole/mod.rs +++ b/crates/oxc_minifier/src/peephole/mod.rs @@ -671,6 +671,12 @@ impl<'a> Traverse<'a> for PeepholeOptimizations { } } + fn enter_call_expression(&mut self, e: &mut CallExpression<'a>, ctx: &mut TraverseCtx<'a>) { + if !ctx.is_tree_shake_only() { + Self::init_iife_parameter_values(e, ctx); + } + } + fn exit_call_expression(&mut self, e: &mut CallExpression<'a>, ctx: &mut TraverseCtx<'a>) { if !ctx.is_tree_shake_only() { Self::substitute_call_expression(e, ctx); diff --git a/crates/oxc_minifier/src/symbol_state.rs b/crates/oxc_minifier/src/symbol_state.rs index 5e3827c071..6132f5a5c6 100644 --- a/crates/oxc_minifier/src/symbol_state.rs +++ b/crates/oxc_minifier/src/symbol_state.rs @@ -57,8 +57,12 @@ impl<'a> SymbolState<'a> { } #[inline] - pub fn init_value(&mut self, symbol_id: SymbolId, value: SymbolValue<'a>) { - self.values[symbol_id] = Some(value); + pub fn init_value( + &mut self, + symbol_id: SymbolId, + value: SymbolValue<'a>, + ) -> &mut SymbolValue<'a> { + self.values[symbol_id].insert(value) } #[inline] diff --git a/crates/oxc_minifier/src/symbol_value.rs b/crates/oxc_minifier/src/symbol_value.rs index bc53a5581b..84879458af 100644 --- a/crates/oxc_minifier/src/symbol_value.rs +++ b/crates/oxc_minifier/src/symbol_value.rs @@ -85,6 +85,11 @@ pub struct SymbolValue<'a> { /// `None` when the value is not a constant evaluated value. pub initialized_constant: Option>, + /// Whether bare reads may be replaced with the constant. IIFE parameter + /// constants guide folding, but their arguments remain at the call site, + /// so replacing bare reads would duplicate the argument's text. + pub allow_constant_inlining: bool, + /// The `initialized_constant` originates from the implicit `undefined` of /// a declaration with no initializer, possibly through direct aliases. /// Textually inlining such a read prints `void 0` — longer than a mangled @@ -125,7 +130,8 @@ impl SymbolValue<'_> { /// [esbuild]: https://github.com/evanw/esbuild/blob/f6058f8364fe7ab91ca57a83e02577ed74c9cae4/internal/js_ast/js_ast.go#L1650-L1685 /// [swc]: https://github.com/swc-project/swc/blob/6c778430811853d4feee2ab3af1473669deb7b2a/crates/swc_ecma_minifier/src/compress/optimize/inline.rs#L277-L295 pub fn can_inline_initialized_constant(&self) -> bool { - if self.references.has_writes() || self.implicit_undefined { + if !self.allow_constant_inlining || self.references.has_writes() || self.implicit_undefined + { return false; } let Some(constant) = &self.initialized_constant else { return false }; diff --git a/crates/oxc_minifier/src/traverse_context/ecma_context.rs b/crates/oxc_minifier/src/traverse_context/ecma_context.rs index 35d4fdbcaf..ed1ea6af81 100644 --- a/crates/oxc_minifier/src/traverse_context/ecma_context.rs +++ b/crates/oxc_minifier/src/traverse_context/ecma_context.rs @@ -313,7 +313,7 @@ impl<'a> TraverseCtx<'a, MinifierState<'a>> { kind: FreshValueKind, falsy_init: bool, implicit_undefined_source: bool, - ) { + ) -> &mut SymbolValue<'a> { let mut references = ReferenceCounts::default(); for reference in self.scoping().get_resolved_references(symbol_id) { references.record(reference.flags()); @@ -365,12 +365,13 @@ impl<'a> TraverseCtx<'a, MinifierState<'a>> { let symbol_value = SymbolValue { initialized_constant, + allow_constant_inlining: true, implicit_undefined, references, kind: if has_multiple_value_declarations { FreshValueKind::None } else { kind }, boolean_falsy, }; - self.state.symbols.init_value(symbol_id, symbol_value); + self.state.symbols.init_value(symbol_id, symbol_value) } /// If two expressions are equal. diff --git a/crates/oxc_minifier/tests/peephole/inline.rs b/crates/oxc_minifier/tests/peephole/inline.rs index 9b51673b7f..4eebe0bd97 100644 --- a/crates/oxc_minifier/tests/peephole/inline.rs +++ b/crates/oxc_minifier/tests/peephole/inline.rs @@ -1,9 +1,140 @@ use oxc_span::SourceType; use crate::{ - CompressOptions, test_options, test_options_source_type, test_same_options, test_smallest, + CompressOptions, test_options, test_options_source_type, test_same_options, + test_same_options_source_type, test_smallest, }; +#[test] +fn inline_constant_iife_arguments() { + // https://github.com/oxc-project/oxc/issues/27379 + test_smallest("const f = x => x + 1; f(1);", ""); + test_smallest("const foo = x => x + 1; foo(1); export const bar = 1;", "export const bar = 1;"); + test_smallest("(x => x + 1)(1);", ""); + test_smallest("((x, y) => x + y)(1, 2);", ""); + test_smallest("(x => x + 1)(1 + 2);", ""); + test_smallest("(x => { return x + 1 })(1);", ""); + test_smallest("(x => x + 'b')('a');", ""); + test_smallest("(x => x + 1n)(1n);", ""); + + // Parameter facts also reach nested closures, respecting shadowing. + test_smallest("use((x => () => x + 1)(1));", "use((x => () => 2)(1));"); + test_smallest("use((x => x + (x => x + 1)(2))(1));", "use((x => x + (x => 3)(2))(1));"); + // Recording the argument must not duplicate it at a bare parameter read. + test_smallest( + "(x => effect(x))('a long string with many characters');", + "(x => effect(x))('a long string with many characters');", + ); +} + +#[test] +fn inline_iife_arguments_preserves_side_effects() { + test_smallest("(x => effect(x + 1))(1);", "(x => effect(2))(1);"); + test_smallest( + "(x => effect(x + 1))((sideEffect(), 1));", + "(x => effect(2))((sideEffect(), 1));", + ); + test_smallest("(x => x + 1)(1, sideEffect());", "(x => 2)(1, sideEffect());"); + + // The type of a nonconstant argument is unknown; a mixed BigInt addition throws. + test_smallest("(x => x + 1)(value);", "(x => x + 1)(value);"); + test_smallest("(x => x + 1)(1n);", "(x => x + 1)(1n);"); + test_smallest("(x => x + 1)(Symbol());", "(x => x + 1)(Symbol());"); + + let options = CompressOptions::smallest(); + for source in [ + "use((x => (x++, () => x))(1));", + "use((x => () => x++)(1));", + "(x => (eval('x = 2'), effect(x)))(1);", + "((x = sideEffect()) => effect(x))(1);", + "(({ x }) => effect(x))(1);", + "((...x) => effect(x))(1);", + "((x, y) => effect(x, y))(...values, 1);", + "((x, y) => effect(x, y))(1, ...values);", + ] { + test_same_options(source, &options); + } + + // Redeclarations may replace the parameter before an earlier closure reads it. + test_smallest( + "(x => { function read() { effect(x) } function x() { sideEffect() } read() })(1);", + "(x => { function read() { effect(x) } function x() { sideEffect() } read() })(1);", + ); +} + +#[test] +fn inline_constant_function_iife_arguments() { + test_smallest("(function (x) { return x + 1 })(1);", ""); + test_smallest("const f = function (x) { return x + 1 }; f(1);", ""); + test_smallest("(function unused(x, y) { return x + y })(1, 2);", ""); + test_smallest("(function (x) { effect(x + 1) })(1);", "(function (x) { effect(2) })(1);"); + test_smallest( + "(function (x) { effect(x + 1) })((sideEffect(), 1));", + "(function (x) { effect(2) })((sideEffect(), 1));", + ); + test_smallest( + "use((function (x) { return () => x + 1 })(1));", + "use((function (x) { return () => 2 })(1));", + ); + test_smallest( + "(function (x) { effect(x) })('a long string with many characters');", + "(function (x) { effect(x) })('a long string with many characters');", + ); + + // Explicit strict mode also makes regular script IIFEs eligible. + test_options_source_type( + "(function (x) { 'use strict'; effect(x + 1) })(1);", + "(function (x) { 'use strict'; effect(2) })(1);", + SourceType::cjs(), + &CompressOptions::smallest(), + ); +} + +#[test] +fn inline_function_iife_arguments_preserves_arguments_aliasing() { + let options = CompressOptions::smallest(); + // Sloppy-mode parameters can change through mapped arguments or caller + // introspection, without a resolved write to the parameter binding. + for source in [ + "(function (x) { effect(x + 1) })(1);", + "(function (x) { arguments[0] = 2, effect(x + 1) })(1);", + "(function (x) { use(arguments), effect(x + 1) })(1);", + "(function (x) { use(() => (arguments[0] = 2, x + 1)) })(1);", + "(function (x) { var arguments; arguments[0] = 2, effect(x + 1) })(1);", + "(function (x, x) { effect(x + 1) })(1, value);", + "(function (x) { mutateCaller(), effect(x + 1) })(1);", + ] { + test_same_options_source_type(source, SourceType::cjs(), &options); + } + + // Strict arguments objects do not alias the parameters, even in closures. + test_smallest( + "(function (x) { arguments[0] = 2, effect(x + 1) })(1);", + "(function (x) { arguments[0] = 2, effect(2) })(1);", + ); + test_smallest( + "use((function (x) { return () => (arguments[0] = 2, x + 1) })(1));", + "use((function (x) { return () => (arguments[0] = 2, 2) })(1));", + ); +} + +#[test] +fn inline_function_iife_arguments_preserves_reentry_and_writes() { + let options = CompressOptions::smallest(); + // The function's own name may be used to call it with different arguments. + for source in [ + "(function f(x) { x && f(0), effect(x + 1) })(1);", + "use((function f(x) { return () => (use(f), x + 1) })(1));", + "(function (x) { use(() => x++), effect(x + 1) })(1);", + "(function (x) { eval('x = 2'), effect(x + 1) })(1);", + "(function (x) { function x() {} effect(x + 1) })(1);", + "(function (x) { return x + 1 })(1n);", + "(function (x) { return x + 1 })(value);", + ] { + test_same_options(source, &options); + } +} + // https://github.com/oxc-project/oxc/issues/24531 // A `var` assigned only inside a conditional holds its hoisted `undefined` // on the untaken path; single-statement block flattening produces a