diff --git a/crates/oxc_ecmascript/src/constant_evaluation/mod.rs b/crates/oxc_ecmascript/src/constant_evaluation/mod.rs index 94d17905559d1..fced608d3942b 100644 --- a/crates/oxc_ecmascript/src/constant_evaluation/mod.rs +++ b/crates/oxc_ecmascript/src/constant_evaluation/mod.rs @@ -99,6 +99,20 @@ pub trait ConstantEvaluation<'a>: MayHaveSideEffects<'a> { } } +impl<'a, T: ConstantEvaluation<'a>> ConstantEvaluation<'a> for Option { + fn evaluate_value(&self, ctx: &impl ConstantEvaluationCtx<'a>) -> Option> { + self.as_ref().and_then(|t| t.evaluate_value(ctx)) + } + + fn evaluate_value_to( + &self, + ctx: &impl ConstantEvaluationCtx<'a>, + target_ty: Option, + ) -> Option> { + self.as_ref().and_then(|t| t.evaluate_value_to(ctx, target_ty)) + } +} + impl<'a> ConstantEvaluation<'a> for IdentifierReference<'a> { fn evaluate_value_to( &self, diff --git a/crates/oxc_ecmascript/src/side_effects/may_have_side_effects.rs b/crates/oxc_ecmascript/src/side_effects/may_have_side_effects.rs index b3c96a976ee76..9bb0725352396 100644 --- a/crates/oxc_ecmascript/src/side_effects/may_have_side_effects.rs +++ b/crates/oxc_ecmascript/src/side_effects/may_have_side_effects.rs @@ -22,6 +22,12 @@ pub trait MayHaveSideEffects<'a> { fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool; } +impl<'a, T: MayHaveSideEffects<'a>> MayHaveSideEffects<'a> for Option { + fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool { + self.as_ref().is_some_and(|t| t.may_have_side_effects(ctx)) + } +} + impl<'a> MayHaveSideEffects<'a> for Expression<'a> { fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool { match self { diff --git a/crates/oxc_minifier/src/ctx.rs b/crates/oxc_minifier/src/ctx.rs index 5f4d17cddb786..b1aa1e209d0b4 100644 --- a/crates/oxc_minifier/src/ctx.rs +++ b/crates/oxc_minifier/src/ctx.rs @@ -7,11 +7,11 @@ use oxc_ecmascript::{ }, side_effects::{MayHaveSideEffects, PropertyReadSideEffects}, }; -use oxc_semantic::{IsGlobalReference, Scoping}; +use oxc_semantic::{IsGlobalReference, Scoping, SymbolId}; use oxc_span::format_atom; use oxc_syntax::reference::ReferenceId; -use crate::{options::CompressOptions, state::MinifierState}; +use crate::{options::CompressOptions, state::MinifierState, symbol_value::SymbolValue}; pub type TraverseCtx<'a> = oxc_traverse::TraverseCtx<'a, MinifierState<'a>>; @@ -50,7 +50,7 @@ impl<'a> oxc_ecmascript::is_global_reference::IsGlobalReference<'a> for Ctx<'a, self.scoping() .get_reference(reference_id) .symbol_id() - .and_then(|symbol_id| self.state.constant_values.get(&symbol_id)) + .and_then(|symbol_id| self.state.symbol_values.get_constant_value(symbol_id)) .cloned() } } @@ -164,6 +164,24 @@ impl<'a> Ctx<'a, '_> { false } + pub fn init_value(&mut self, symbol_id: SymbolId, constant: ConstantValue<'a>) { + let mut read_references_count = 0; + let mut write_references_count = 0; + for r in self.scoping().get_resolved_references(symbol_id) { + if r.is_read() { + read_references_count += 1; + } + if r.is_write() { + write_references_count += 1; + } + } + + let scope_id = self.scoping.current_scope_id(); + let symbol_value = + SymbolValue { constant, read_references_count, write_references_count, scope_id }; + self.state.symbol_values.init_value(symbol_id, symbol_value); + } + /// If two expressions are equal. /// Special case `undefined` == `void 0` pub fn expr_eq(&self, a: &Expression<'a>, b: &Expression<'a>) -> bool { diff --git a/crates/oxc_minifier/src/lib.rs b/crates/oxc_minifier/src/lib.rs index 842c4b7a86993..4adcd7b38abbd 100644 --- a/crates/oxc_minifier/src/lib.rs +++ b/crates/oxc_minifier/src/lib.rs @@ -8,6 +8,7 @@ mod keep_var; mod options; mod peephole; mod state; +mod symbol_value; #[cfg(test)] mod tester; diff --git a/crates/oxc_minifier/src/peephole/fold_constants.rs b/crates/oxc_minifier/src/peephole/fold_constants.rs index c2b0f1710d06a..ac5f2acbfdf2b 100644 --- a/crates/oxc_minifier/src/peephole/fold_constants.rs +++ b/crates/oxc_minifier/src/peephole/fold_constants.rs @@ -7,7 +7,6 @@ use oxc_ecmascript::{ }; use oxc_span::GetSpan; use oxc_syntax::operator::{BinaryOperator, LogicalOperator}; -use oxc_traverse::Ancestor; use crate::ctx::Ctx; @@ -40,20 +39,6 @@ impl<'a> PeepholeOptimizations { *expr = folded_expr; ctx.state.changed = true; } - - // Save `const value = false` into constant values. - if let Ancestor::VariableDeclaratorInit(decl) = ctx.parent() { - // TODO: Check for no write references. - if decl.kind().is_const() { - if let BindingPatternKind::BindingIdentifier(ident) = &decl.id().kind { - // TODO: refactor all the above code to return value instead of expression, to avoid calling `evaluate_value` again. - if let Some(value) = expr.evaluate_value(ctx) { - let symbol_id = ident.symbol_id(); - ctx.state.constant_values.insert(symbol_id, value); - } - } - } - } } #[expect(clippy::float_cmp)] diff --git a/crates/oxc_minifier/src/peephole/inline.rs b/crates/oxc_minifier/src/peephole/inline.rs new file mode 100644 index 0000000000000..0f35c9ae555c6 --- /dev/null +++ b/crates/oxc_minifier/src/peephole/inline.rs @@ -0,0 +1,55 @@ +use oxc_ast::ast::*; +use oxc_ecmascript::constant_evaluation::ConstantEvaluation; +use oxc_span::GetSpan; + +use crate::ctx::Ctx; + +use super::PeepholeOptimizations; + +impl<'a> PeepholeOptimizations { + pub fn init_symbol_value(&self, decl: &VariableDeclarator<'a>, ctx: &mut Ctx<'a, '_>) { + let BindingPatternKind::BindingIdentifier(ident) = &decl.id.kind else { return }; + let Some(symbol_id) = ident.symbol_id.get() else { return }; + // Skip if not `const` variable. + if !ctx.scoping().symbol_flags(symbol_id).is_const_variable() { + return; + } + let Some(value) = decl.init.evaluate_value(ctx) else { return }; + ctx.init_value(symbol_id, value); + } + + pub fn inline_identifier_reference(&self, expr: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) { + let Expression::Identifier(ident) = expr else { return }; + let Some(reference_id) = ident.reference_id.get() else { return }; + let Some(symbol_id) = ctx.scoping().get_reference(reference_id).symbol_id() else { return }; + let Some(symbol_value) = ctx.state.symbol_values.get_symbol_value(symbol_id) else { + return; + }; + // Only inline single reference (for now). + if symbol_value.read_references_count > 1 { + return; + } + // Skip if there are write references. + if symbol_value.write_references_count > 0 { + return; + } + *expr = ctx.value_to_expr(expr.span(), symbol_value.constant.clone()); + ctx.state.changed = true; + } +} + +#[cfg(test)] +mod test { + use crate::{ + CompressOptions, + tester::{test_options, test_same}, + }; + + #[test] + fn r#const() { + let options = CompressOptions::smallest(); + test_options("const foo = 1; log(foo)", "log(1)", &options); + test_options("export const foo = 1; log(foo)", "export const foo = 1; log(1)", &options); + test_same("const foo = 1; log(foo), log(foo)"); + } +} diff --git a/crates/oxc_minifier/src/peephole/mod.rs b/crates/oxc_minifier/src/peephole/mod.rs index c130808e1b5a3..4ed324af4a329 100644 --- a/crates/oxc_minifier/src/peephole/mod.rs +++ b/crates/oxc_minifier/src/peephole/mod.rs @@ -2,6 +2,7 @@ mod convert_to_dotted_properties; mod fold_constants; +mod inline; mod minimize_conditional_expression; mod minimize_conditions; mod minimize_expression_in_boolean_context; @@ -102,10 +103,12 @@ impl<'a> PeepholeOptimizations { impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations { fn enter_program(&mut self, _program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { + ctx.state.symbol_values.clear(); ctx.state.changed = false; } fn exit_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { + // Remove unused references by visiting the AST again and diff the collected references. let refs_before = ctx.scoping().resolved_references().flatten().copied().collect::>(); let mut counter = ReferencesCounter::default(); @@ -155,18 +158,26 @@ impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations { ctx: &mut TraverseCtx<'a>, ) { let mut ctx = Ctx::new(ctx); - self.substitute_variable_declaration(decl, &mut ctx); } - fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + fn exit_variable_declarator( + &mut self, + decl: &mut VariableDeclarator<'a>, + ctx: &mut TraverseCtx<'a>, + ) { let mut ctx = Ctx::new(ctx); + self.init_symbol_value(decl, &mut ctx); + } + fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + let mut ctx = Ctx::new(ctx); self.fold_constants_exit_expression(expr, &mut ctx); self.minimize_conditions_exit_expression(expr, &mut ctx); self.remove_dead_code_exit_expression(expr, &mut ctx); self.replace_known_methods_exit_expression(expr, &mut ctx); self.substitute_exit_expression(expr, &mut ctx); + self.inline_identifier_reference(expr, &mut ctx); } fn exit_unary_expression(&mut self, expr: &mut UnaryExpression<'a>, ctx: &mut TraverseCtx<'a>) { diff --git a/crates/oxc_minifier/src/state.rs b/crates/oxc_minifier/src/state.rs index d89f94e42f46b..330d0e19986c6 100644 --- a/crates/oxc_minifier/src/state.rs +++ b/crates/oxc_minifier/src/state.rs @@ -1,25 +1,20 @@ -use rustc_hash::{FxHashMap, FxHashSet}; +use rustc_hash::FxHashSet; -use oxc_ecmascript::constant_evaluation::ConstantValue; -use oxc_semantic::SymbolId; use oxc_span::SourceType; +use oxc_syntax::symbol::SymbolId; -use crate::CompressOptions; +use crate::{CompressOptions, symbol_value::SymbolValues}; pub struct MinifierState<'a> { pub source_type: SourceType, pub options: CompressOptions, - /// Constant values evaluated from expressions. - /// - /// Values are saved during constant evaluation phase. - /// Values are read during [oxc_ecmascript::is_global_reference::IsGlobalReference::get_constant_value_for_reference_id]. - pub constant_values: FxHashMap>, - /// Function declarations that are empty pub empty_functions: FxHashSet, + pub symbol_values: SymbolValues<'a>, + pub changed: bool, } @@ -28,8 +23,8 @@ impl MinifierState<'_> { Self { source_type, options, - constant_values: FxHashMap::default(), empty_functions: FxHashSet::default(), + symbol_values: SymbolValues::default(), changed: false, } } diff --git a/crates/oxc_minifier/src/symbol_value.rs b/crates/oxc_minifier/src/symbol_value.rs new file mode 100644 index 0000000000000..e530a50a6985b --- /dev/null +++ b/crates/oxc_minifier/src/symbol_value.rs @@ -0,0 +1,39 @@ +use rustc_hash::FxHashMap; + +use oxc_ecmascript::constant_evaluation::ConstantValue; +use oxc_syntax::{scope::ScopeId, symbol::SymbolId}; + +#[derive(Debug)] +pub struct SymbolValue<'a> { + /// Constant value evaluated from expressions. + pub constant: ConstantValue<'a>, + + pub read_references_count: u32, + pub write_references_count: u32, + + #[expect(unused)] + pub scope_id: ScopeId, +} + +#[derive(Debug, Default)] +pub struct SymbolValues<'a> { + values: FxHashMap>, +} + +impl<'a> SymbolValues<'a> { + pub fn clear(&mut self) { + self.values.clear(); + } + + pub fn init_value(&mut self, symbol_id: SymbolId, symbol_value: SymbolValue<'a>) { + self.values.insert(symbol_id, symbol_value); + } + + pub fn get_constant_value(&self, symbol_id: SymbolId) -> Option<&ConstantValue<'a>> { + self.values.get(&symbol_id).map(|v| &v.constant) + } + + pub fn get_symbol_value(&self, symbol_id: SymbolId) -> Option<&SymbolValue<'a>> { + self.values.get(&symbol_id) + } +} diff --git a/tasks/coverage/snapshots/minifier_test262.snap b/tasks/coverage/snapshots/minifier_test262.snap index b236892570408..2f8cc34f025f3 100644 --- a/tasks/coverage/snapshots/minifier_test262.snap +++ b/tasks/coverage/snapshots/minifier_test262.snap @@ -2,4 +2,10 @@ commit: 4b5d36ab minifier_test262 Summary: AST Parsed : 42013/42013 (100.00%) -Positive Passed: 42013/42013 (100.00%) +Positive Passed: 42010/42013 (99.99%) +Compress: tasks/coverage/test262/test/intl402/Temporal/PlainDate/prototype/toLocaleString/lone-options-accepted.js + +Compress: tasks/coverage/test262/test/intl402/Temporal/PlainMonthDay/prototype/toLocaleString/lone-options-accepted.js + +Compress: tasks/coverage/test262/test/intl402/Temporal/PlainYearMonth/prototype/toLocaleString/lone-options-accepted.js + diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index 54ba1fad84756..8379b09eecddf 100644 --- a/tasks/minsize/minsize.snap +++ b/tasks/minsize/minsize.snap @@ -11,9 +11,9 @@ Original | minified | minified | gzip | gzip | Fixture 544.10 kB | 71.38 kB | 72.48 kB | 25.85 kB | 26.20 kB | lodash.js -555.77 kB | 270.91 kB | 270.13 kB | 88.27 kB | 90.80 kB | d3.js +555.77 kB | 270.90 kB | 270.13 kB | 88.25 kB | 90.80 kB | d3.js -1.01 MB | 440.19 kB | 458.89 kB | 122.39 kB | 126.71 kB | bundle.min.js +1.01 MB | 440.15 kB | 458.89 kB | 122.37 kB | 126.71 kB | bundle.min.js 1.25 MB | 647.00 kB | 646.76 kB | 160.27 kB | 163.73 kB | three.js