Skip to content

Commit 4945e52

Browse files
refactor(minifier): Implement PR review feedback for parameter inlining
Address code review comments from lukesandberg (PR #11156, review #3378066428): 1. Replace FxHashSet<usize> with sorted Vec<usize> for inlined_params - Simpler data structure since indices are already populated in sorted order - More efficient iteration during argument removal 2. Optimize implicit undefined handling with Option<Option<Box<Expr>>> - Avoids allocating Expr::Ident for undefined on each iteration - Only allocates when actually adding to params_to_inline - Clearer semantics: None = implicit undefined, Some(Some(expr)) = explicit value 3. Remove Option wrapper from Vec<Box<Expr>> in usage analyzer - Analyzer only inserts Some values, never None - Simplifies type signature: Vec<Vec<Box<Expr>>> instead of Vec<Vec<Option<Box<Expr>>>> 4. Simplify to always use 'let' instead of tracking reassignment - Preserves parameter semantics (always reassignable) - Reduces complexity by avoiding reassignment analysis - May enable better constant folding in other passes 5. Document NaN handling in expr_eq - Added doc comments explaining why custom equality is needed - NaN is treated as equal to NaN for parameter inlining - Context-aware identifier comparison for unresolved "undefined" 6. Simplify identifier comparison in expr_eq - Use naive comparison since is_safe_constant_for_param_inline already validates - Avoids repeating validation logic Related: #10931
1 parent a29bf4b commit 4945e52

File tree

5 files changed

+74
-70
lines changed

5 files changed

+74
-70
lines changed

crates/swc_ecma_minifier/src/compress/optimize/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,8 +244,9 @@ struct Optimizer<'a> {
244244
functions: Box<FxHashMap<Id, FnMetadata>>,
245245

246246
/// Tracks which parameter indices have been inlined for each function.
247-
/// Maps function ID to a set of parameter indices that were removed.
248-
inlined_params: Box<FxHashMap<Id, FxHashSet<usize>>>,
247+
/// Maps function ID to a sorted vector of parameter indices that were
248+
/// removed.
249+
inlined_params: Box<FxHashMap<Id, Vec<usize>>>,
249250
}
250251

251252
#[derive(Default)]

crates/swc_ecma_minifier/src/compress/optimize/params.rs

Lines changed: 63 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use rustc_hash::FxHashSet;
21
use swc_common::DUMMY_SP;
32
use swc_ecma_ast::*;
43

@@ -104,59 +103,48 @@ impl Optimizer<'_> {
104103
}
105104

106105
// Analyze each parameter
107-
let mut params_to_inline: Vec<(usize, Box<Expr>, bool)> = Vec::new();
106+
let mut params_to_inline: Vec<(usize, Box<Expr>)> = Vec::new();
108107

109108
for (param_idx, param) in f.params.iter().enumerate() {
110109
// Only handle simple identifier parameters
111-
let param_id = match &param.pat {
110+
let _param_id = match &param.pat {
112111
Pat::Ident(ident) => ident.id.to_id(),
113112
_ => continue, // Skip destructuring, rest params, etc.
114113
};
115114

116-
// Check if parameter is reassigned within the function
117-
// If so, we'll use 'let' instead of 'const'
118-
let is_reassigned = if let Some(param_usage) = self.data.vars.get(&param_id) {
119-
param_usage.flags.contains(VarUsageInfoFlags::REASSIGNED)
120-
} else {
121-
false
122-
};
123-
124115
// Collect argument values for this parameter across all call sites
125-
let mut common_value: Option<Box<Expr>> = None;
116+
// Use Option<Option<Box<Expr>>> to track:
117+
// - None = no common value yet established
118+
// - Some(None) = all call sites have implicit undefined
119+
// - Some(Some(expr)) = all call sites have the same explicit expression
120+
let mut common_value: Option<Option<Box<Expr>>> = None;
126121
let mut inlinable = true;
127122

128123
for call_site in call_sites {
129-
let arg_value = if param_idx < call_site.len() {
130-
call_site[param_idx].clone()
124+
// Get argument at this position, or None if implicit undefined
125+
let arg_value: Option<Box<Expr>> = if param_idx < call_site.len() {
126+
Some(call_site[param_idx].clone())
131127
} else {
132-
// Implicit undefined - use unresolved context
133-
Some(Box::new(Expr::Ident(Ident::new(
134-
"undefined".into(),
135-
DUMMY_SP,
136-
self.ctx.expr_ctx.unresolved_ctxt,
137-
))))
128+
None // Implicit undefined
138129
};
139130

140-
let arg_value = match arg_value {
141-
Some(v) => v,
142-
None => {
131+
// Check if this is a safe, constant value to inline
132+
if let Some(ref expr) = arg_value {
133+
if !self.is_safe_constant_for_param_inline(expr) {
143134
inlinable = false;
144135
break;
145136
}
146-
};
147-
148-
// Check if this is a safe, constant value to inline
149-
if !self.is_safe_constant_for_param_inline(&arg_value) {
150-
inlinable = false;
151-
break;
152137
}
138+
// Implicit undefined (None) is always safe to inline
153139

154140
match &common_value {
155141
None => {
142+
// First call site, establish the common value
156143
common_value = Some(arg_value);
157144
}
158145
Some(existing) => {
159-
if !self.expr_eq(existing, &arg_value) {
146+
// Check if this call site has the same value
147+
if !self.arg_eq(existing, &arg_value) {
160148
inlinable = false;
161149
break;
162150
}
@@ -166,8 +154,17 @@ impl Optimizer<'_> {
166154

167155
// If all call sites pass the same constant value, mark for inlining
168156
if inlinable {
169-
if let Some(value) = common_value {
170-
params_to_inline.push((param_idx, value, is_reassigned));
157+
if let Some(Some(value)) = common_value {
158+
// Explicit value passed at all call sites
159+
params_to_inline.push((param_idx, value));
160+
} else if let Some(None) = common_value {
161+
// All call sites have implicit undefined
162+
let undefined_expr = Box::new(Expr::Ident(Ident::new(
163+
"undefined".into(),
164+
DUMMY_SP,
165+
self.ctx.expr_ctx.unresolved_ctxt,
166+
)));
167+
params_to_inline.push((param_idx, undefined_expr));
171168
}
172169
}
173170
}
@@ -182,7 +179,7 @@ impl Optimizer<'_> {
182179
fn apply_param_inlining(
183180
&mut self,
184181
f: &mut Function,
185-
params_to_inline: &[(usize, Box<Expr>, bool)],
182+
params_to_inline: &[(usize, Box<Expr>)],
186183
fn_id: &Id,
187184
) {
188185
// Sort in reverse order to remove from the end first
@@ -192,25 +189,20 @@ impl Optimizer<'_> {
192189
// Collect variable declarations to add at the beginning of function body
193190
let mut var_decls = Vec::new();
194191

195-
// Track which parameter indices are being inlined
196-
let mut inlined_indices = FxHashSet::default();
192+
// Track which parameter indices are being inlined (will be sorted)
193+
let mut inlined_indices = Vec::new();
197194

198-
for (param_idx, value, is_reassigned) in &sorted_params {
199-
inlined_indices.insert(*param_idx);
195+
for (param_idx, value) in &sorted_params {
196+
inlined_indices.push(*param_idx);
200197

201198
if let Some(param) = f.params.get(*param_idx) {
202199
if let Pat::Ident(ident) = &param.pat {
203-
// Create variable declaration: const/let paramName = value;
204-
// Use 'let' if the parameter is reassigned, 'const' otherwise
205-
let var_kind = if *is_reassigned {
206-
VarDeclKind::Let
207-
} else {
208-
VarDeclKind::Const
209-
};
200+
// Create variable declaration: let paramName = value;
201+
// Always use 'let' to preserve parameter semantics (reassignable)
210202
var_decls.push(Stmt::Decl(Decl::Var(Box::new(VarDecl {
211203
span: DUMMY_SP,
212204
ctxt: Default::default(),
213-
kind: var_kind,
205+
kind: VarDeclKind::Let,
214206
declare: false,
215207
decls: vec![VarDeclarator {
216208
span: DUMMY_SP,
@@ -224,7 +216,7 @@ impl Optimizer<'_> {
224216
}
225217

226218
// Remove parameters (in reverse order)
227-
for (param_idx, _, _) in &sorted_params {
219+
for (param_idx, _) in &sorted_params {
228220
f.params.remove(*param_idx);
229221
}
230222

@@ -236,7 +228,8 @@ impl Optimizer<'_> {
236228
}
237229

238230
// Store the inlined parameter indices for later use when visiting call
239-
// sites
231+
// sites. Reverse sort to get descending order for efficient removal.
232+
inlined_indices.sort_by(|a, b| b.cmp(a));
240233
self.inlined_params.insert(fn_id.clone(), inlined_indices);
241234

242235
self.changed = true;
@@ -283,14 +276,31 @@ impl Optimizer<'_> {
283276
}
284277
}
285278

279+
/// Compare two optional argument values for equality.
280+
/// None represents implicit undefined.
281+
fn arg_eq(&self, a: &Option<Box<Expr>>, b: &Option<Box<Expr>>) -> bool {
282+
match (a, b) {
283+
(None, None) => true, // Both implicit undefined
284+
(Some(a_expr), Some(b_expr)) => self.expr_eq(a_expr, b_expr),
285+
_ => false, // One is implicit undefined, the other is not
286+
}
287+
}
288+
286289
/// Compare two expressions for equality (structural equality for simple
287290
/// cases).
291+
///
292+
/// We cannot use the derived `Eq` trait because:
293+
/// 1. NaN handling: We treat NaN as equal to NaN for parameter inlining
294+
/// purposes, whereas standard floating point comparison has NaN != NaN.
295+
/// 2. Context-aware identifier comparison: We only consider unresolved
296+
/// "undefined" identifiers as safe to inline, checking the syntax
297+
/// context.
288298
fn expr_eq(&self, a: &Expr, b: &Expr) -> bool {
289299
match (a, b) {
290300
(Expr::Lit(Lit::Null(_)), Expr::Lit(Lit::Null(_))) => true,
291301
(Expr::Lit(Lit::Bool(a)), Expr::Lit(Lit::Bool(b))) => a.value == b.value,
292302
(Expr::Lit(Lit::Num(a)), Expr::Lit(Lit::Num(b))) => {
293-
// Handle NaN specially
303+
// Handle NaN specially: treat NaN as equal to NaN for parameter inlining
294304
if a.value.is_nan() && b.value.is_nan() {
295305
true
296306
} else {
@@ -299,15 +309,9 @@ impl Optimizer<'_> {
299309
}
300310
(Expr::Lit(Lit::Str(a)), Expr::Lit(Lit::Str(b))) => a.value == b.value,
301311
(Expr::Lit(Lit::BigInt(a)), Expr::Lit(Lit::BigInt(b))) => a.value == b.value,
302-
// Compare identifiers:
303-
// Only allow unresolved "undefined" identifiers
304-
(Expr::Ident(a), Expr::Ident(b)) => {
305-
let a_is_unresolved = a.ctxt == self.ctx.expr_ctx.unresolved_ctxt;
306-
let b_is_unresolved = b.ctxt == self.ctx.expr_ctx.unresolved_ctxt;
307-
308-
// Both must be unresolved "undefined"
309-
a_is_unresolved && b_is_unresolved && a.sym == "undefined" && b.sym == "undefined"
310-
}
312+
// Compare identifiers: we can compare naively since is_safe_constant_for_param_inline
313+
// already validated that only unresolved "undefined" identifiers reach here
314+
(Expr::Ident(a), Expr::Ident(b)) => a.sym == b.sym && a.ctxt == b.ctxt,
311315
(
312316
Expr::Unary(UnaryExpr {
313317
op: op_a,
@@ -348,11 +352,8 @@ impl Optimizer<'_> {
348352
};
349353

350354
// Remove arguments at the inlined parameter indices
351-
// We need to iterate in reverse order to avoid index shifting issues
352-
let mut indices_to_remove: Vec<usize> = inlined_indices.iter().copied().collect();
353-
indices_to_remove.sort_by(|a, b| b.cmp(a)); // Sort in descending order
354-
355-
for idx in indices_to_remove {
355+
// The indices are already sorted in descending order from apply_param_inlining
356+
for &idx in inlined_indices {
356357
if idx < call.args.len() {
357358
call.args.remove(idx);
358359
self.changed = true;

crates/swc_ecma_minifier/src/program_data.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,11 @@ pub(crate) struct VarUsageInfo {
135135
pub(crate) accessed_props: FxHashMap<Wtf8Atom, u32>,
136136
pub(crate) accessed_props: FxHashMap<Atom, u32>,
137137

138-
/// Tracks call sites for functions. Maps parameter index to list of
139-
/// argument expressions. Used for parameter inlining optimization.
140-
pub(crate) call_site_args: Option<Vec<Vec<Option<Box<Expr>>>>>,
138+
/// Tracks call sites for functions. Each inner Vec contains the arguments
139+
/// passed at that call site. Used for parameter inlining optimization.
140+
/// Arguments beyond the call site's actual argument count are implicitly
141+
/// undefined.
142+
pub(crate) call_site_args: Option<Vec<Vec<Box<Expr>>>>,
141143
}
142144

143145
impl Default for VarUsageInfo {
@@ -566,7 +568,7 @@ impl Storage for ProgramData {
566568
self.vars.get(&id).map(|v| v.as_ref())
567569
}
568570

569-
fn record_call_site_args(&mut self, callee_id: Id, args: Vec<Option<Box<Expr>>>) {
571+
fn record_call_site_args(&mut self, callee_id: Id, args: Vec<Box<Expr>>) {
570572
let var = self.vars.entry(callee_id).or_default();
571573

572574
// Initialize the call_site_args if it doesn't exist

crates/swc_ecma_usage_analyzer/src/analyzer/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ where
445445
call_args.clear();
446446
break;
447447
}
448-
call_args.push(Some(arg.expr.clone()));
448+
call_args.push(arg.expr.clone());
449449
}
450450

451451
// Only record if we have a valid argument list (no spread)

crates/swc_ecma_usage_analyzer/src/analyzer/storage.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ pub trait Storage: Sized + Default {
4545

4646
/// Records arguments passed to a function at a call site.
4747
/// Used for parameter inlining optimization.
48-
fn record_call_site_args(&mut self, callee_id: Id, args: Vec<Option<Box<Expr>>>);
48+
fn record_call_site_args(&mut self, callee_id: Id, args: Vec<Box<Expr>>);
4949
}
5050

5151
pub trait ScopeDataLike: Sized + Default + Clone {

0 commit comments

Comments
 (0)