feat(minifier): inline const variables that are only used once (#12488)

Just to get the infrastructure ready.

Other "inline"s are more complicated due to TDZ.
This commit is contained in:
Boshen committed 2025-07-24 08:08:10 +00:00
1 parent 3f6edc96ce
commit b877039e43
11 files changed
+164 -34

No files matched your search

@@ -99,6 +99,20 @@ pub trait ConstantEvaluation<'a>: MayHaveSideEffects<'a> {
}
}
impl<'a, T: ConstantEvaluation<'a>> ConstantEvaluation<'a> for Option<T> {
fn evaluate_value(&self, ctx: &impl ConstantEvaluationCtx<'a>) -> Option<ConstantValue<'a>> {
self.as_ref().and_then(|t| t.evaluate_value(ctx))
}
fn evaluate_value_to(
&self,
ctx: &impl ConstantEvaluationCtx<'a>,
target_ty: Option<ValueType>,
) -> Option<ConstantValue<'a>> {
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,
@@ -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<T> {
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 {
+21 -3
View File
@@ -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 {
+1
View File
@@ -8,6 +8,7 @@ mod keep_var;
mod options;
mod peephole;
mod state;
mod symbol_value;
#[cfg(test)]
mod tester;
@@ -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)]
@@ -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)");
}
}
+13 -2
View File
@@ -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::<FxHashSet<_>>();
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_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>) {
+6 -11
View File
@@ -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<SymbolId, ConstantValue<'a>>,
/// Function declarations that are empty
pub empty_functions: FxHashSet<SymbolId>,
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,
}
}
+39
View File
@@ -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<SymbolId, SymbolValue<'a>>,
}
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)
}
}
@@ -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
+2 -2
View File
@@ -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