Skip to content

Commit 775229a

Browse files
oxc-botBoshen
authored andcommitted
feat(minifier): inline const variables that are only used once.
Just to get the infrastructure ready. Other "inline"s are more complicated due to TDZ.
1 parent ccf1fb4 commit 775229a

File tree

10 files changed

+157
-33
lines changed

10 files changed

+157
-33
lines changed

crates/oxc_ecmascript/src/constant_evaluation/mod.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,20 @@ pub trait ConstantEvaluation<'a>: MayHaveSideEffects<'a> {
9999
}
100100
}
101101

102+
impl<'a, T: ConstantEvaluation<'a>> ConstantEvaluation<'a> for Option<T> {
103+
fn evaluate_value(&self, ctx: &impl ConstantEvaluationCtx<'a>) -> Option<ConstantValue<'a>> {
104+
self.as_ref().and_then(|t| t.evaluate_value(ctx))
105+
}
106+
107+
fn evaluate_value_to(
108+
&self,
109+
ctx: &impl ConstantEvaluationCtx<'a>,
110+
target_ty: Option<ValueType>,
111+
) -> Option<ConstantValue<'a>> {
112+
self.as_ref().and_then(|t| t.evaluate_value_to(ctx, target_ty))
113+
}
114+
}
115+
102116
impl<'a> ConstantEvaluation<'a> for IdentifierReference<'a> {
103117
fn evaluate_value_to(
104118
&self,

crates/oxc_ecmascript/src/side_effects/may_have_side_effects.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ pub trait MayHaveSideEffects<'a> {
2222
fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool;
2323
}
2424

25+
impl<'a, T: MayHaveSideEffects<'a>> MayHaveSideEffects<'a> for Option<T> {
26+
fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool {
27+
self.as_ref().is_some_and(|t| t.may_have_side_effects(ctx))
28+
}
29+
}
30+
2531
impl<'a> MayHaveSideEffects<'a> for Expression<'a> {
2632
fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool {
2733
match self {

crates/oxc_minifier/src/ctx.rs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ use oxc_ecmascript::{
77
},
88
side_effects::{MayHaveSideEffects, PropertyReadSideEffects},
99
};
10-
use oxc_semantic::{IsGlobalReference, Scoping};
10+
use oxc_semantic::{IsGlobalReference, Scoping, SymbolId};
1111
use oxc_span::format_atom;
1212
use oxc_syntax::reference::ReferenceId;
1313

14-
use crate::{options::CompressOptions, state::MinifierState};
14+
use crate::{options::CompressOptions, state::MinifierState, symbol_value::SymbolValue};
1515

1616
pub type TraverseCtx<'a> = oxc_traverse::TraverseCtx<'a, MinifierState<'a>>;
1717

@@ -50,7 +50,7 @@ impl<'a> oxc_ecmascript::is_global_reference::IsGlobalReference<'a> for Ctx<'a,
5050
self.scoping()
5151
.get_reference(reference_id)
5252
.symbol_id()
53-
.and_then(|symbol_id| self.state.constant_values.get(&symbol_id))
53+
.and_then(|symbol_id| self.state.symbol_values.get_constant_value(symbol_id))
5454
.cloned()
5555
}
5656
}
@@ -164,6 +164,24 @@ impl<'a> Ctx<'a, '_> {
164164
false
165165
}
166166

167+
pub fn init_value(&mut self, symbol_id: SymbolId, constant: ConstantValue<'a>) {
168+
let mut read_references_count = 0;
169+
let mut write_references_count = 0;
170+
for r in self.scoping().get_resolved_references(symbol_id) {
171+
if r.is_read() {
172+
read_references_count += 1;
173+
}
174+
if r.is_write() {
175+
write_references_count += 1;
176+
}
177+
}
178+
179+
let scope_id = self.scoping.current_scope_id();
180+
let symbol_value =
181+
SymbolValue { constant, read_references_count, write_references_count, scope_id };
182+
self.state.symbol_values.init_value(symbol_id, symbol_value);
183+
}
184+
167185
/// If two expressions are equal.
168186
/// Special case `undefined` == `void 0`
169187
pub fn expr_eq(&self, a: &Expression<'a>, b: &Expression<'a>) -> bool {

crates/oxc_minifier/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ mod keep_var;
88
mod options;
99
mod peephole;
1010
mod state;
11+
mod symbol_value;
1112

1213
#[cfg(test)]
1314
mod tester;

crates/oxc_minifier/src/peephole/fold_constants.rs

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ use oxc_ecmascript::{
77
};
88
use oxc_span::GetSpan;
99
use oxc_syntax::operator::{BinaryOperator, LogicalOperator};
10-
use oxc_traverse::Ancestor;
1110

1211
use crate::ctx::Ctx;
1312

@@ -40,20 +39,6 @@ impl<'a> PeepholeOptimizations {
4039
*expr = folded_expr;
4140
ctx.state.changed = true;
4241
}
43-
44-
// Save `const value = false` into constant values.
45-
if let Ancestor::VariableDeclaratorInit(decl) = ctx.parent() {
46-
// TODO: Check for no write references.
47-
if decl.kind().is_const() {
48-
if let BindingPatternKind::BindingIdentifier(ident) = &decl.id().kind {
49-
// TODO: refactor all the above code to return value instead of expression, to avoid calling `evaluate_value` again.
50-
if let Some(value) = expr.evaluate_value(ctx) {
51-
let symbol_id = ident.symbol_id();
52-
ctx.state.constant_values.insert(symbol_id, value);
53-
}
54-
}
55-
}
56-
}
5742
}
5843

5944
#[expect(clippy::float_cmp)]
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
use oxc_ast::ast::*;
2+
use oxc_ecmascript::constant_evaluation::ConstantEvaluation;
3+
use oxc_span::GetSpan;
4+
5+
use crate::ctx::Ctx;
6+
7+
use super::PeepholeOptimizations;
8+
9+
impl<'a> PeepholeOptimizations {
10+
pub fn init_symbol_value(&self, decl: &VariableDeclarator<'a>, ctx: &mut Ctx<'a, '_>) {
11+
let BindingPatternKind::BindingIdentifier(ident) = &decl.id.kind else { return };
12+
let Some(symbol_id) = ident.symbol_id.get() else { return };
13+
// Skip if not `const` variable.
14+
if !ctx.scoping().symbol_flags(symbol_id).is_const_variable() {
15+
return;
16+
}
17+
let Some(value) = decl.init.evaluate_value(ctx) else { return };
18+
ctx.init_value(symbol_id, value);
19+
}
20+
21+
pub fn inline_identifier_reference(&self, expr: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
22+
let Expression::Identifier(ident) = expr else { return };
23+
let Some(reference_id) = ident.reference_id.get() else { return };
24+
let Some(symbol_id) = ctx.scoping().get_reference(reference_id).symbol_id() else { return };
25+
let Some(symbol_value) = ctx.state.symbol_values.get_symbol_value(symbol_id) else {
26+
return;
27+
};
28+
// Only inline single reference (for now).
29+
if symbol_value.read_references_count > 1 {
30+
return;
31+
}
32+
// Skip if there are write references.
33+
if symbol_value.write_references_count > 0 {
34+
return;
35+
}
36+
*expr = ctx.value_to_expr(expr.span(), symbol_value.constant.clone());
37+
ctx.state.changed = true;
38+
}
39+
}
40+
41+
#[cfg(test)]
42+
mod test {
43+
use crate::{
44+
CompressOptions,
45+
tester::{test_options, test_same},
46+
};
47+
48+
#[test]
49+
fn r#const() {
50+
let options = CompressOptions::smallest();
51+
test_options("const foo = 1; log(foo)", "log(1)", &options);
52+
test_options("export const foo = 1; log(foo)", "export const foo = 1; log(1)", &options);
53+
test_same("const foo = 1; log(foo), log(foo)");
54+
}
55+
}

crates/oxc_minifier/src/peephole/mod.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
mod convert_to_dotted_properties;
44
mod fold_constants;
5+
mod inline;
56
mod minimize_conditional_expression;
67
mod minimize_conditions;
78
mod minimize_expression_in_boolean_context;
@@ -102,10 +103,12 @@ impl<'a> PeepholeOptimizations {
102103

103104
impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations {
104105
fn enter_program(&mut self, _program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
106+
ctx.state.symbol_values.clear();
105107
ctx.state.changed = false;
106108
}
107109

108110
fn exit_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
111+
// Remove unused references by visiting the AST again and diff the collected references.
109112
let refs_before =
110113
ctx.scoping().resolved_references().flatten().copied().collect::<FxHashSet<_>>();
111114
let mut counter = ReferencesCounter::default();
@@ -155,18 +158,26 @@ impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations {
155158
ctx: &mut TraverseCtx<'a>,
156159
) {
157160
let mut ctx = Ctx::new(ctx);
158-
159161
self.substitute_variable_declaration(decl, &mut ctx);
160162
}
161163

162-
fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
164+
fn exit_variable_declarator(
165+
&mut self,
166+
decl: &mut VariableDeclarator<'a>,
167+
ctx: &mut TraverseCtx<'a>,
168+
) {
163169
let mut ctx = Ctx::new(ctx);
170+
self.init_symbol_value(decl, &mut ctx);
171+
}
164172

173+
fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
174+
let mut ctx = Ctx::new(ctx);
165175
self.fold_constants_exit_expression(expr, &mut ctx);
166176
self.minimize_conditions_exit_expression(expr, &mut ctx);
167177
self.remove_dead_code_exit_expression(expr, &mut ctx);
168178
self.replace_known_methods_exit_expression(expr, &mut ctx);
169179
self.substitute_exit_expression(expr, &mut ctx);
180+
self.inline_identifier_reference(expr, &mut ctx);
170181
}
171182

172183
fn exit_unary_expression(&mut self, expr: &mut UnaryExpression<'a>, ctx: &mut TraverseCtx<'a>) {

crates/oxc_minifier/src/state.rs

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,20 @@
1-
use rustc_hash::{FxHashMap, FxHashSet};
1+
use rustc_hash::FxHashSet;
22

3-
use oxc_ecmascript::constant_evaluation::ConstantValue;
4-
use oxc_semantic::SymbolId;
53
use oxc_span::SourceType;
4+
use oxc_syntax::symbol::SymbolId;
65

7-
use crate::CompressOptions;
6+
use crate::{CompressOptions, symbol_value::SymbolValues};
87

98
pub struct MinifierState<'a> {
109
pub source_type: SourceType,
1110

1211
pub options: CompressOptions,
1312

14-
/// Constant values evaluated from expressions.
15-
///
16-
/// Values are saved during constant evaluation phase.
17-
/// Values are read during [oxc_ecmascript::is_global_reference::IsGlobalReference::get_constant_value_for_reference_id].
18-
pub constant_values: FxHashMap<SymbolId, ConstantValue<'a>>,
19-
2013
/// Function declarations that are empty
2114
pub empty_functions: FxHashSet<SymbolId>,
2215

16+
pub symbol_values: SymbolValues<'a>,
17+
2318
pub changed: bool,
2419
}
2520

@@ -28,8 +23,8 @@ impl MinifierState<'_> {
2823
Self {
2924
source_type,
3025
options,
31-
constant_values: FxHashMap::default(),
3226
empty_functions: FxHashSet::default(),
27+
symbol_values: SymbolValues::default(),
3328
changed: false,
3429
}
3530
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
use rustc_hash::FxHashMap;
2+
3+
use oxc_ecmascript::constant_evaluation::ConstantValue;
4+
use oxc_syntax::{scope::ScopeId, symbol::SymbolId};
5+
6+
#[derive(Debug)]
7+
pub struct SymbolValue<'a> {
8+
/// Constant value evaluated from expressions.
9+
pub constant: ConstantValue<'a>,
10+
11+
pub read_references_count: u32,
12+
pub write_references_count: u32,
13+
14+
#[allow(unused)]
15+
pub scope_id: ScopeId,
16+
}
17+
18+
#[derive(Debug, Default)]
19+
pub struct SymbolValues<'a> {
20+
values: FxHashMap<SymbolId, SymbolValue<'a>>,
21+
}
22+
23+
impl<'a> SymbolValues<'a> {
24+
pub fn clear(&mut self) {
25+
self.values.clear();
26+
}
27+
28+
pub fn init_value(&mut self, symbol_id: SymbolId, symbol_value: SymbolValue<'a>) {
29+
self.values.insert(symbol_id, symbol_value);
30+
}
31+
32+
pub fn get_constant_value(&self, symbol_id: SymbolId) -> Option<&ConstantValue<'a>> {
33+
self.values.get(&symbol_id).map(|v| &v.constant)
34+
}
35+
36+
pub fn get_symbol_value(&self, symbol_id: SymbolId) -> Option<&SymbolValue<'a>> {
37+
self.values.get(&symbol_id)
38+
}
39+
}

tasks/minsize/minsize.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ Original | minified | minified | gzip | gzip | Fixture
1111

1212
544.10 kB | 71.38 kB | 72.48 kB | 25.85 kB | 26.20 kB | lodash.js
1313

14-
555.77 kB | 270.91 kB | 270.13 kB | 88.27 kB | 90.80 kB | d3.js
14+
555.77 kB | 270.90 kB | 270.13 kB | 88.25 kB | 90.80 kB | d3.js
1515

16-
1.01 MB | 440.19 kB | 458.89 kB | 122.39 kB | 126.71 kB | bundle.min.js
16+
1.01 MB | 440.15 kB | 458.89 kB | 122.37 kB | 126.71 kB | bundle.min.js
1717

1818
1.25 MB | 647.00 kB | 646.76 kB | 160.27 kB | 163.73 kB | three.js
1919

0 commit comments

Comments
 (0)