Skip to content

Commit d6fc750

Browse files
committed
feat(minifier): add pure to side-effect free global constructor during DCE (oxc-project#11270)
So that codegen can print /* #__PURE__# */ during codegen. This mimics esbuild evanw/esbuild@7918716 closes ENG-48
1 parent 17671ef commit d6fc750

File tree

4 files changed

+102
-72
lines changed

4 files changed

+102
-72
lines changed

crates/oxc_minifier/src/peephole/normalize.rs

Lines changed: 80 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
use oxc_allocator::{TakeIn, Vec};
22
use oxc_ast::ast::*;
3+
use oxc_ecmascript::{
4+
constant_evaluation::{DetermineValueType, ValueType},
5+
side_effects::MayHaveSideEffects,
6+
};
37
use oxc_semantic::IsGlobalReference;
48
use oxc_span::GetSpan;
59
use oxc_syntax::scope::ScopeFlags;
@@ -25,6 +29,7 @@ pub struct NormalizeOptions {
2529
/// * convert `NaN` to `f64::NaN`
2630
/// * convert `var x; void x` to `void 0`
2731
/// * convert `undefined` to `void 0`
32+
/// * apply `pure` to side-effect free global constructors (e.g. `new WeakMap()`)
2833
///
2934
/// Also
3035
///
@@ -94,11 +99,11 @@ impl<'a> Traverse<'a> for Normalize {
9499
}
95100

96101
fn exit_call_expression(&mut self, e: &mut CallExpression<'a>, ctx: &mut TraverseCtx<'a>) {
97-
Self::set_no_side_effects(&mut e.pure, &e.callee, ctx);
102+
Self::set_no_side_effects_to_call_expr(e, ctx);
98103
}
99104

100105
fn exit_new_expression(&mut self, e: &mut NewExpression<'a>, ctx: &mut TraverseCtx<'a>) {
101-
Self::set_no_side_effects(&mut e.pure, &e.callee, ctx);
106+
Self::set_pure_or_no_side_effects_to_new_expr(e, ctx);
102107
}
103108
}
104109

@@ -243,19 +248,82 @@ impl<'a> Normalize {
243248
e.argument = ctx.ast.expression_numeric_literal(ident.span, 0.0, None, NumberBase::Decimal);
244249
}
245250

246-
fn set_no_side_effects(pure: &mut bool, callee: &Expression<'a>, ctx: &TraverseCtx<'a>) {
247-
if !*pure {
248-
if let Some(ident) = callee.get_identifier_reference() {
249-
if let Some(symbol_id) =
250-
ctx.scoping().get_reference(ident.reference_id()).symbol_id()
251-
{
252-
if ctx.scoping().no_side_effects().contains(&symbol_id) {
253-
*pure = true;
254-
}
255-
}
251+
fn set_no_side_effects_to_call_expr(call_expr: &mut CallExpression<'a>, ctx: &TraverseCtx<'a>) {
252+
if call_expr.pure {
253+
return;
254+
}
255+
let Some(ident) = call_expr.callee.get_identifier_reference() else {
256+
return;
257+
};
258+
if let Some(symbol_id) = ctx.scoping().get_reference(ident.reference_id()).symbol_id() {
259+
// Apply `/* #__NO_SIDE_EFFECTS__ */`
260+
if ctx.scoping().no_side_effects().contains(&symbol_id) {
261+
call_expr.pure = true;
256262
}
257263
}
258264
}
265+
266+
fn set_pure_or_no_side_effects_to_new_expr(
267+
new_expr: &mut NewExpression<'a>,
268+
ctx: &TraverseCtx<'a>,
269+
) {
270+
if new_expr.pure {
271+
return;
272+
}
273+
let Some(ident) = new_expr.callee.get_identifier_reference() else {
274+
return;
275+
};
276+
if let Some(symbol_id) = ctx.scoping().get_reference(ident.reference_id()).symbol_id() {
277+
// Apply `/* #__NO_SIDE_EFFECTS__ */`
278+
if ctx.scoping().no_side_effects().contains(&symbol_id) {
279+
new_expr.pure = true;
280+
}
281+
return;
282+
}
283+
// callee is a global reference.
284+
let ctx = Ctx(ctx);
285+
let len = new_expr.arguments.len();
286+
if match ident.name.as_str() {
287+
"WeakSet" | "WeakMap" if ctx.is_global_reference(ident) => match len {
288+
0 => true,
289+
1 => match new_expr.arguments[0].as_expression() {
290+
Some(Expression::NullLiteral(_)) => true,
291+
Some(Expression::ArrayExpression(e)) => e.elements.is_empty(),
292+
Some(e) if ctx.is_expression_undefined(e) => true,
293+
_ => false,
294+
},
295+
_ => false,
296+
},
297+
"Date" if ctx.is_global_reference(ident) => match len {
298+
0 => true,
299+
1 => {
300+
let Some(arg) = new_expr.arguments[0].as_expression() else { return };
301+
let ty = arg.value_type(&ctx);
302+
matches!(
303+
ty,
304+
ValueType::Null
305+
| ValueType::Undefined
306+
| ValueType::Boolean
307+
| ValueType::Number
308+
| ValueType::String
309+
) && !arg.may_have_side_effects(&ctx)
310+
}
311+
_ => false,
312+
},
313+
"Set" | "Map" if ctx.is_global_reference(ident) => match len {
314+
0 => true,
315+
1 => match new_expr.arguments[0].as_expression() {
316+
Some(Expression::NullLiteral(_)) => true,
317+
Some(e) if ctx.is_expression_undefined(e) => true,
318+
_ => false,
319+
},
320+
_ => false,
321+
},
322+
_ => false,
323+
} {
324+
new_expr.pure = true;
325+
}
326+
}
259327
}
260328

261329
#[cfg(test)]

crates/oxc_minifier/src/peephole/remove_unused_expression.rs

Lines changed: 11 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,7 @@ use std::iter;
22

33
use oxc_allocator::{TakeIn, Vec};
44
use oxc_ast::ast::*;
5-
use oxc_ecmascript::{
6-
ToPrimitive,
7-
constant_evaluation::{DetermineValueType, ValueType},
8-
side_effects::MayHaveSideEffects,
9-
};
5+
use oxc_ecmascript::{ToPrimitive, side_effects::MayHaveSideEffects};
106
use oxc_span::GetSpan;
117
use oxc_syntax::es_target::ESTarget;
128

@@ -247,64 +243,20 @@ impl<'a> PeepholeOptimizations {
247243
ctx: Ctx<'a, '_>,
248244
) -> bool {
249245
let Expression::NewExpression(new_expr) = e else { return false };
250-
251-
if new_expr.pure {
252-
let mut exprs =
253-
self.fold_arguments_into_needed_expressions(&mut new_expr.arguments, state, ctx);
254-
if exprs.is_empty() {
255-
return true;
256-
} else if exprs.len() == 1 {
257-
*e = exprs.pop().unwrap();
258-
state.changed = true;
259-
return false;
260-
}
261-
*e = ctx.ast.expression_sequence(new_expr.span, exprs);
262-
state.changed = true;
246+
if !new_expr.pure {
263247
return false;
264248
}
265-
266-
let Expression::Identifier(ident) = &new_expr.callee else { return false };
267-
let len = new_expr.arguments.len();
268-
if match ident.name.as_str() {
269-
"WeakSet" | "WeakMap" if ctx.is_global_reference(ident) => match len {
270-
0 => true,
271-
1 => match new_expr.arguments[0].as_expression() {
272-
Some(Expression::NullLiteral(_)) => true,
273-
Some(Expression::ArrayExpression(e)) => e.elements.is_empty(),
274-
Some(e) if ctx.is_expression_undefined(e) => true,
275-
_ => false,
276-
},
277-
_ => false,
278-
},
279-
"Date" if ctx.is_global_reference(ident) => match len {
280-
0 => true,
281-
1 => {
282-
let Some(arg) = new_expr.arguments[0].as_expression() else { return false };
283-
let ty = arg.value_type(&ctx);
284-
matches!(
285-
ty,
286-
ValueType::Null
287-
| ValueType::Undefined
288-
| ValueType::Boolean
289-
| ValueType::Number
290-
| ValueType::String
291-
) && !arg.may_have_side_effects(&ctx)
292-
}
293-
_ => false,
294-
},
295-
"Set" | "Map" if ctx.is_global_reference(ident) => match len {
296-
0 => true,
297-
1 => match new_expr.arguments[0].as_expression() {
298-
Some(Expression::NullLiteral(_)) => true,
299-
Some(e) if ctx.is_expression_undefined(e) => true,
300-
_ => false,
301-
},
302-
_ => false,
303-
},
304-
_ => false,
305-
} {
249+
let mut exprs =
250+
self.fold_arguments_into_needed_expressions(&mut new_expr.arguments, state, ctx);
251+
if exprs.is_empty() {
306252
return true;
253+
} else if exprs.len() == 1 {
254+
*e = exprs.pop().unwrap();
255+
state.changed = true;
256+
return false;
307257
}
258+
*e = ctx.ast.expression_sequence(new_expr.span, exprs);
259+
state.changed = true;
308260
false
309261
}
310262

crates/oxc_minifier/tests/peephole/dead_code_elimination.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,14 @@ fn dce_var_hoisting() {
175175
);
176176
}
177177

178+
#[test]
179+
fn pure_comment_for_pure_global_constructors() {
180+
test("var x = new WeakSet", "var x = /* @__PURE__ */ new WeakSet();\n");
181+
test("var x = new WeakSet(null)", "var x = /* @__PURE__ */ new WeakSet(null);\n");
182+
test("var x = new WeakSet(undefined)", "var x = /* @__PURE__ */ new WeakSet(void 0);\n");
183+
test("var x = new WeakSet([])", "var x = /* @__PURE__ */ new WeakSet([]);\n");
184+
}
185+
178186
// https://github.com/terser/terser/blob/v5.9.0/test/compress/dead-code.js
179187
#[test]
180188
fn dce_from_terser() {

tasks/coverage/snapshots/minifier_test262.snap

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@ commit: 4b5d36ab
22

33
minifier_test262 Summary:
44
AST Parsed : 42013/42013 (100.00%)
5-
Positive Passed: 42013/42013 (100.00%)
5+
Positive Passed: 42012/42013 (100.00%)
6+
Compress: tasks/coverage/test262/test/built-ins/Date/prototype/valueOf/S9.4_A3_T2.js
7+

0 commit comments

Comments
 (0)