mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
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:
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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)");
|
||||
}
|
||||
}
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user