Skip to content

Commit

Permalink
feat(minifier): merge expressions in for-in statement head (#8811)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
sapphi-red committed Jan 31, 2025
1 parent adb8ebd commit d9f1d0d
Show file tree
Hide file tree
Showing 4 changed files with 48 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -202,18 +202,17 @@ mod test {
}

#[test]
#[ignore]
fn test_for_in() {
test("var a; for(a in b) foo()", "for (var a in b) foo()");
// test("var a; for(a in b) foo()", "for (var a in b) foo()");
test("a = 0; for(a in b) foo()", "for (a in a = 0, b) foo();");
test_same("var a = 0; for(a in b) foo()");
// test_same("var a = 0; for(a in b) foo()");

// We don't handle labels yet.
test_same("var a; a:for(a in b) foo()");
test_same("var a; a:b:for(a in b) foo()");
// test_same("var a; a:for(a in b) foo()");
// test_same("var a; a:b:for(a in b) foo()");

// Verify FOR inside IFs.
test("if(x){var a; for(a in b) foo()}", "if(x) for(var a in b) foo()");
// test("if(x){var a; for(a in b) foo()}", "if(x) for(var a in b) foo()");

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

// We don't handle destructuring patterns yet.
test("var a; var b; for ([a, b] in c) foo();", "var a, b; for ([a, b] in c) foo();");
// test("var a; var b; for ([a, b] in c) foo();", "var a, b; for ([a, b] in c) foo();");
}

#[test]
Expand Down
32 changes: 32 additions & 0 deletions crates/oxc_minifier/src/peephole/minimize_statements.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use oxc_allocator::Vec;
use oxc_ast::{ast::*, Visit};
use oxc_ecmascript::side_effects::MayHaveSideEffects;
use oxc_span::{cmp::ContentEq, GetSpan};
use oxc_traverse::Ancestor;

Expand Down Expand Up @@ -340,6 +341,37 @@ impl<'a> PeepholeOptimizations {
}
result.push(Statement::ForStatement(for_stmt));
}
Statement::ForInStatement(mut for_in_stmt) => {
// "a; for (var b in c) d" => "for (var b in a, c) d"
if let Some(Statement::ExpressionStatement(prev_expr_stmt)) = result.last_mut() {
// Annex B.3.5 allows initializers in non-strict mode
// <https://tc39.es/ecma262/multipage/additional-ecmascript-features-for-web-browsers.html#sec-initializers-in-forin-statement-heads>
// If there's a side-effectful initializer, we should not move the previous statement inside.
let has_side_effectful_initializer = {
if let ForStatementLeft::VariableDeclaration(var_decl) = &for_in_stmt.left {
if var_decl.declarations.len() == 1 {
// only var can have a initializer
var_decl.kind.is_var()
&& var_decl.declarations[0].init.as_ref().is_some_and(|init| {
ctx.expression_may_have_side_effects(init)
})
} else {
// the spec does not allow multiple declarations though
true
}
} else {
false
}
};
if !has_side_effectful_initializer {
let a = &mut prev_expr_stmt.expression;
for_in_stmt.right = Self::join_sequence(a, &mut for_in_stmt.right, ctx);
result.pop();
self.mark_current_function_as_changed();
}
}
result.push(Statement::ForInStatement(for_in_stmt));
}
stmt => result.push(stmt),
}
}
Expand Down
6 changes: 5 additions & 1 deletion crates/oxc_minifier/src/peephole/statement_fusion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,18 @@ mod test {
}

#[test]
#[ignore]
fn fuse_into_for_in1() {
test("a;b;c;for(x in y){}", "for(x in a,b,c,y);");
}

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

#[test]
Expand Down
10 changes: 5 additions & 5 deletions tasks/minsize/minsize.snap
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,23 @@ Original | minified | minified | gzip | gzip | Fixture

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

287.63 kB | 89.49 kB | 90.07 kB | 30.95 kB | 31.95 kB | jquery.js
287.63 kB | 89.48 kB | 90.07 kB | 30.95 kB | 31.95 kB | jquery.js

342.15 kB | 117.68 kB | 118.14 kB | 43.56 kB | 44.37 kB | vue.js
342.15 kB | 117.68 kB | 118.14 kB | 43.57 kB | 44.37 kB | vue.js

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

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

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

1.25 MB | 650.36 kB | 646.76 kB | 161.02 kB | 163.73 kB | three.js
1.25 MB | 650.36 kB | 646.76 kB | 161.01 kB | 163.73 kB | three.js

2.14 MB | 718.61 kB | 724.14 kB | 162.14 kB | 181.07 kB | victory.js
2.14 MB | 718.61 kB | 724.14 kB | 162.13 kB | 181.07 kB | victory.js

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

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

10.95 MB | 3.37 MB | 3.49 MB | 863.73 kB | 915.50 kB | typescript.js
10.95 MB | 3.37 MB | 3.49 MB | 863.74 kB | 915.50 kB | typescript.js

0 comments on commit d9f1d0d

Please sign in to comment.