Skip to content

Commit d9f1d0d

Browse files
committed
feat(minifier): merge expressions in for-in statement head (#8811)
Compress `a; for (var b in c) d` into `for (var b in a, c) d`. This is possible when the left hand does not have a sideeffectful initializer. (Initializers on the left hand of for-in is Annex B thing.) **References** - [Spec of `ForIn/OfHeadEvaluation`](https://tc39.es/ecma262/multipage/ecmascript-language-statements-and-declarations.html#sec-runtime-semantics-forinofheadevaluation): `c` in the example above is passed as `expr` to this abstract operation. No side effect exists before Step 3. - [Spec of the initializer in ForIn](https://tc39.es/ecma262/multipage/additional-ecmascript-features-for-web-browsers.html#sec-initializers-in-forin-statement-heads): See "The runtime semantics of ForInOfLoopEvaluation in 14.7.5.5 are augmented with the following:" part. Evaluation of Initializer is executed before `ForIn/OfHeadEvalution`. This is the reason why it cannot be compressed when a sideeffectful initializer exists.
1 parent adb8ebd commit d9f1d0d

File tree

4 files changed

+48
-13
lines changed

4 files changed

+48
-13
lines changed

crates/oxc_minifier/src/peephole/collapse_variable_declarations.rs

+6-7
Original file line numberDiff line numberDiff line change
@@ -202,18 +202,17 @@ mod test {
202202
}
203203

204204
#[test]
205-
#[ignore]
206205
fn test_for_in() {
207-
test("var a; for(a in b) foo()", "for (var a in b) foo()");
206+
// test("var a; for(a in b) foo()", "for (var a in b) foo()");
208207
test("a = 0; for(a in b) foo()", "for (a in a = 0, b) foo();");
209-
test_same("var a = 0; for(a in b) foo()");
208+
// test_same("var a = 0; for(a in b) foo()");
210209

211210
// We don't handle labels yet.
212-
test_same("var a; a:for(a in b) foo()");
213-
test_same("var a; a:b:for(a in b) foo()");
211+
// test_same("var a; a:for(a in b) foo()");
212+
// test_same("var a; a:b:for(a in b) foo()");
214213

215214
// Verify FOR inside IFs.
216-
test("if(x){var a; for(a in b) foo()}", "if(x) for(var a in b) foo()");
215+
// test("if(x){var a; for(a in b) foo()}", "if(x) for(var a in b) foo()");
217216

218217
// Any other expression.
219218
test("init(); for(a in b) foo()", "for (a in init(), b) foo();");
@@ -222,7 +221,7 @@ mod test {
222221
test_same("function f(){ for(a in b) foo() }");
223222

224223
// We don't handle destructuring patterns yet.
225-
test("var a; var b; for ([a, b] in c) foo();", "var a, b; for ([a, b] in c) foo();");
224+
// test("var a; var b; for ([a, b] in c) foo();", "var a, b; for ([a, b] in c) foo();");
226225
}
227226

228227
#[test]

crates/oxc_minifier/src/peephole/minimize_statements.rs

+32
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use oxc_allocator::Vec;
22
use oxc_ast::{ast::*, Visit};
3+
use oxc_ecmascript::side_effects::MayHaveSideEffects;
34
use oxc_span::{cmp::ContentEq, GetSpan};
45
use oxc_traverse::Ancestor;
56

@@ -340,6 +341,37 @@ impl<'a> PeepholeOptimizations {
340341
}
341342
result.push(Statement::ForStatement(for_stmt));
342343
}
344+
Statement::ForInStatement(mut for_in_stmt) => {
345+
// "a; for (var b in c) d" => "for (var b in a, c) d"
346+
if let Some(Statement::ExpressionStatement(prev_expr_stmt)) = result.last_mut() {
347+
// Annex B.3.5 allows initializers in non-strict mode
348+
// <https://tc39.es/ecma262/multipage/additional-ecmascript-features-for-web-browsers.html#sec-initializers-in-forin-statement-heads>
349+
// If there's a side-effectful initializer, we should not move the previous statement inside.
350+
let has_side_effectful_initializer = {
351+
if let ForStatementLeft::VariableDeclaration(var_decl) = &for_in_stmt.left {
352+
if var_decl.declarations.len() == 1 {
353+
// only var can have a initializer
354+
var_decl.kind.is_var()
355+
&& var_decl.declarations[0].init.as_ref().is_some_and(|init| {
356+
ctx.expression_may_have_side_effects(init)
357+
})
358+
} else {
359+
// the spec does not allow multiple declarations though
360+
true
361+
}
362+
} else {
363+
false
364+
}
365+
};
366+
if !has_side_effectful_initializer {
367+
let a = &mut prev_expr_stmt.expression;
368+
for_in_stmt.right = Self::join_sequence(a, &mut for_in_stmt.right, ctx);
369+
result.pop();
370+
self.mark_current_function_as_changed();
371+
}
372+
}
373+
result.push(Statement::ForInStatement(for_in_stmt));
374+
}
343375
stmt => result.push(stmt),
344376
}
345377
}

crates/oxc_minifier/src/peephole/statement_fusion.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,18 @@ mod test {
4545
}
4646

4747
#[test]
48-
#[ignore]
4948
fn fuse_into_for_in1() {
5049
test("a;b;c;for(x in y){}", "for(x in a,b,c,y);");
5150
}
5251

5352
#[test]
5453
fn fuse_into_for_in2() {
54+
// this should not be compressed into `for (var x = a() in b(), [0])`
55+
// as the side effect order of `a()` and `b()` changes
5556
test_same("a();for(var x = b() in y);");
57+
test("a = 1; for(var x = 2 in y);", "for(var x = 2 in a = 1, y);");
58+
// this can be compressed because b() runs after a()
59+
test("a(); for (var { x = b() } in y);", "for (var { x = b() } in a(), y);");
5660
}
5761

5862
#[test]

tasks/minsize/minsize.snap

+5-5
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,23 @@ Original | minified | minified | gzip | gzip | Fixture
55

66
173.90 kB | 59.55 kB | 59.82 kB | 19.19 kB | 19.33 kB | moment.js
77

8-
287.63 kB | 89.49 kB | 90.07 kB | 30.95 kB | 31.95 kB | jquery.js
8+
287.63 kB | 89.48 kB | 90.07 kB | 30.95 kB | 31.95 kB | jquery.js
99

10-
342.15 kB | 117.68 kB | 118.14 kB | 43.56 kB | 44.37 kB | vue.js
10+
342.15 kB | 117.68 kB | 118.14 kB | 43.57 kB | 44.37 kB | vue.js
1111

1212
544.10 kB | 71.43 kB | 72.48 kB | 25.87 kB | 26.20 kB | lodash.js
1313

1414
555.77 kB | 271.25 kB | 270.13 kB | 88.35 kB | 90.80 kB | d3.js
1515

1616
1.01 MB | 440.96 kB | 458.89 kB | 122.50 kB | 126.71 kB | bundle.min.js
1717

18-
1.25 MB | 650.36 kB | 646.76 kB | 161.02 kB | 163.73 kB | three.js
18+
1.25 MB | 650.36 kB | 646.76 kB | 161.01 kB | 163.73 kB | three.js
1919

20-
2.14 MB | 718.61 kB | 724.14 kB | 162.14 kB | 181.07 kB | victory.js
20+
2.14 MB | 718.61 kB | 724.14 kB | 162.13 kB | 181.07 kB | victory.js
2121

2222
3.20 MB | 1.01 MB | 1.01 MB | 324.31 kB | 331.56 kB | echarts.js
2323

2424
6.69 MB | 2.30 MB | 2.31 MB | 468.88 kB | 488.28 kB | antd.js
2525

26-
10.95 MB | 3.37 MB | 3.49 MB | 863.73 kB | 915.50 kB | typescript.js
26+
10.95 MB | 3.37 MB | 3.49 MB | 863.74 kB | 915.50 kB | typescript.js
2727

0 commit comments

Comments
 (0)