Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 62 additions & 19 deletions crates/oxc_minifier/src/peephole/inline.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use oxc_ast::ast::*;
use oxc_ecmascript::constant_evaluation::{ConstantEvaluation, ConstantValue};
use oxc_semantic::SymbolId;
use oxc_semantic::{ScopeFlags, ScopeId, SymbolId};
use oxc_span::GetSpan;
use oxc_traverse::Ancestor;
use rustc_hash::FxHashSet;
Expand Down Expand Up @@ -83,13 +83,12 @@ impl<'a> PeepholeOptimizations {
write_references_count += 1;
}
}
let scope_id = ctx.scoping().symbol_scope_id(symbol_id);
let value = SymbolInformation {
value: SymbolValue::default(),
exported: exported_values.contains(&symbol_id),
read_references_count,
write_references_count,
scope_id,
scope_id: None,
};
ctx.state.symbol_values.init_value(symbol_id, value);
}
Expand All @@ -98,15 +97,25 @@ impl<'a> PeepholeOptimizations {
pub fn init_symbol_value(decl: &VariableDeclarator<'a>, ctx: &mut Ctx<'a, '_>) {
let BindingPatternKind::BindingIdentifier(ident) = &decl.id.kind else { return };
let symbol_id = ident.symbol_id();
if decl.kind.is_var() || Self::is_for_statement_init(ctx) {
// - Skip constant value inlining for `var` declarations, due to TDZ problems.
// - Set None for for statement initializers as the value of these are set by the for statement.
// Set None for for statement initializers as the value of these are set by the for statement.
if Self::is_for_statement_init(ctx) {
return;
}

let value =
decl.init.as_ref().map_or(Some(ConstantValue::Undefined), |e| e.evaluate_value(ctx));
ctx.state.symbol_values.set_constant_value(symbol_id, value);
let init = decl.init.as_ref();
let symbol_value = if let Some(value) =
init.map_or(Some(ConstantValue::Undefined), |e| e.evaluate_value(ctx))
{
if decl.kind.is_var() {
SymbolValue::ScopedPrimitive(value)
} else {
SymbolValue::Primitive(value)
}
} else {
return;
};
let current_scope_id = ctx.current_scope_id();
ctx.state.symbol_values.set_value(symbol_id, symbol_value, current_scope_id);
}

fn is_for_statement_init(ctx: &Ctx<'a, '_>) -> bool {
Expand All @@ -124,19 +133,53 @@ impl<'a> PeepholeOptimizations {
if symbol_value.write_references_count > 0 {
return;
}
let SymbolValue::Primitive(cv) = &symbol_value.value else { return };
if symbol_value.read_references_count == 1
|| match cv {
ConstantValue::Number(n) => n.fract() == 0.0 && *n >= -99.0 && *n <= 999.0,
ConstantValue::BigInt(_) => false,
ConstantValue::String(s) => s.len() <= 3,
ConstantValue::Boolean(_) | ConstantValue::Undefined | ConstantValue::Null => true,
match &symbol_value.value {
SymbolValue::Primitive(cv) => {
if symbol_value.read_references_count == 1
|| Self::can_inline_constant_multiple_times(cv)
{
*expr = ctx.value_to_expr(expr.span(), cv.clone());
ctx.state.changed = true;
}
}
{
*expr = ctx.value_to_expr(expr.span(), cv.clone());
ctx.state.changed = true;
SymbolValue::ScopedPrimitive(cv) => {
if (symbol_value.read_references_count == 1
|| Self::can_inline_constant_multiple_times(cv))
&& symbol_value.scope_id.is_some_and(|declared_scope_id| {
Self::is_referenced_in_same_hoist_scope(declared_scope_id, ctx)
})
{
*expr = ctx.value_to_expr(expr.span(), cv.clone());
ctx.state.changed = true;
}
}
SymbolValue::Unknown => {}
}
}

fn can_inline_constant_multiple_times(cv: &ConstantValue<'_>) -> bool {
match cv {
ConstantValue::Number(n) => n.fract() == 0.0 && *n >= -99.0 && *n <= 999.0,
ConstantValue::BigInt(_) => false,
ConstantValue::String(s) => s.len() <= 3,
ConstantValue::Boolean(_) | ConstantValue::Undefined | ConstantValue::Null => true,
}
}

fn is_referenced_in_same_hoist_scope(declared_scope_id: ScopeId, ctx: &Ctx<'a, '_>) -> bool {
ctx.scoping()
.scope_ancestors(ctx.current_scope_id())
.find_map(|scope_id| {
if declared_scope_id == scope_id {
return Some(true);
}
if ctx.scoping().scope_flags(scope_id).contains(ScopeFlags::Var) {
return Some(false);
}
None
})
.unwrap_or_default()
}
}

#[cfg(test)]
Expand Down
18 changes: 10 additions & 8 deletions crates/oxc_minifier/src/symbol_value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ use oxc_syntax::{scope::ScopeId, symbol::SymbolId};

#[derive(Debug, Default)]
pub enum SymbolValue<'a> {
/// Initialized primitive constant value evaluated from expressions.
/// Initialized primitive constant value.
Primitive(ConstantValue<'a>),
/// Initialized primitive value.
/// This can be inlined within the same scope after the variable is declared.
ScopedPrimitive(ConstantValue<'a>),
#[default]
Unknown,
}
Expand All @@ -27,8 +30,7 @@ pub struct SymbolInformation<'a> {
pub read_references_count: u32,
pub write_references_count: u32,

#[expect(unused)]
pub scope_id: ScopeId,
pub scope_id: Option<ScopeId>,
}

#[derive(Debug, Default)]
Expand All @@ -45,15 +47,15 @@ impl<'a> SymbolInformationMap<'a> {
self.values.insert(symbol_id, symbol_value);
}

pub fn set_constant_value(
pub fn set_value(
&mut self,
symbol_id: SymbolId,
symbol_value: Option<ConstantValue<'a>>,
symbol_value: SymbolValue<'a>,
scope_id: ScopeId,
) {
let info = self.values.get_mut(&symbol_id).expect("symbol value must exist");
if let Some(constant) = symbol_value {
info.value = SymbolValue::Primitive(constant);
}
info.value = symbol_value;
info.scope_id = Some(scope_id);
}

pub fn get_symbol_value(&self, symbol_id: SymbolId) -> Option<&SymbolInformation<'a>> {
Expand Down
24 changes: 12 additions & 12 deletions tasks/minsize/minsize.snap
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
| Oxc | ESBuild | Oxc | ESBuild |
Original | minified | minified | gzip | gzip | Iterations | File
-------------------------------------------------------------------------------------
72.14 kB | 23.21 kB | 23.70 kB | 8.38 kB | 8.54 kB | 2 | react.development.js
72.14 kB | 23.14 kB | 23.70 kB | 8.32 kB | 8.54 kB | 2 | react.development.js

173.90 kB | 59.44 kB | 59.82 kB | 19.16 kB | 19.33 kB | 2 | moment.js
173.90 kB | 59.33 kB | 59.82 kB | 19.08 kB | 19.33 kB | 2 | moment.js

287.63 kB | 89.28 kB | 90.07 kB | 30.94 kB | 31.95 kB | 2 | jquery.js
287.63 kB | 89.28 kB | 90.07 kB | 30.93 kB | 31.95 kB | 2 | jquery.js

342.15 kB | 117.01 kB | 118.14 kB | 43.19 kB | 44.37 kB | 2 | vue.js
342.15 kB | 116.94 kB | 118.14 kB | 43.14 kB | 44.37 kB | 2 | vue.js

544.10 kB | 71.18 kB | 72.48 kB | 25.85 kB | 26.20 kB | 2 | lodash.js
544.10 kB | 71.89 kB | 72.48 kB | 25.48 kB | 26.20 kB | 2 | lodash.js

555.77 kB | 270.78 kB | 270.13 kB | 88.19 kB | 90.80 kB | 2 | d3.js
555.77 kB | 270.62 kB | 270.13 kB | 88.13 kB | 90.80 kB | 2 | d3.js

1.01 MB | 439.58 kB | 458.89 kB | 122.15 kB | 126.71 kB | 2 | bundle.min.js
1.01 MB | 439.47 kB | 458.89 kB | 122.10 kB | 126.71 kB | 4 | bundle.min.js

1.25 MB | 645.63 kB | 646.76 kB | 159.54 kB | 163.73 kB | 2 | three.js
1.25 MB | 644.77 kB | 646.76 kB | 158.99 kB | 163.73 kB | 2 | three.js

2.14 MB | 713.54 kB | 724.14 kB | 161.00 kB | 181.07 kB | 2 | victory.js
2.14 MB | 713.33 kB | 724.14 kB | 160.72 kB | 181.07 kB | 2 | victory.js

3.20 MB | 1.00 MB | 1.01 MB | 323.12 kB | 331.56 kB | 3 | echarts.js
3.20 MB | 1.00 MB | 1.01 MB | 322.70 kB | 331.56 kB | 3 | echarts.js

6.69 MB | 2.22 MB | 2.31 MB | 459.28 kB | 488.28 kB | 4 | antd.js
6.69 MB | 2.22 MB | 2.31 MB | 458.78 kB | 488.28 kB | 4 | antd.js

10.95 MB | 3.34 MB | 3.49 MB | 855.24 kB | 915.50 kB | 4 | typescript.js
10.95 MB | 3.34 MB | 3.49 MB | 854.83 kB | 915.50 kB | 4 | typescript.js

2 changes: 1 addition & 1 deletion tasks/track_memory_allocations/allocs_minifier.snap
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ RadixUIAdoptionSection.jsx | 2.52 kB || 85 | 4 |

pdf.mjs | 567.30 kB || 19599 | 2910 || 47403 | 7782 | 1.624 MB

antd.js | 6.69 MB || 99860 | 13518 || 331725 | 70354 | 17.408 MB
antd.js | 6.69 MB || 99745 | 13520 || 331917 | 70164 | 17.367 MB

Loading