From d9f1d0daa1acfbe5b62f6bbfaa92511b4cdaf2ed Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Fri, 31 Jan 2025 15:31:24 +0000 Subject: [PATCH] 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. --- .../collapse_variable_declarations.rs | 13 ++++---- .../src/peephole/minimize_statements.rs | 32 +++++++++++++++++++ .../src/peephole/statement_fusion.rs | 6 +++- tasks/minsize/minsize.snap | 10 +++--- 4 files changed, 48 insertions(+), 13 deletions(-) diff --git a/crates/oxc_minifier/src/peephole/collapse_variable_declarations.rs b/crates/oxc_minifier/src/peephole/collapse_variable_declarations.rs index bfecb7300763f..0b01955ca0aab 100644 --- a/crates/oxc_minifier/src/peephole/collapse_variable_declarations.rs +++ b/crates/oxc_minifier/src/peephole/collapse_variable_declarations.rs @@ -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();"); @@ -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] diff --git a/crates/oxc_minifier/src/peephole/minimize_statements.rs b/crates/oxc_minifier/src/peephole/minimize_statements.rs index a4919f2b20133..dca3ad4ced95c 100644 --- a/crates/oxc_minifier/src/peephole/minimize_statements.rs +++ b/crates/oxc_minifier/src/peephole/minimize_statements.rs @@ -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; @@ -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 + // + // 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), } } diff --git a/crates/oxc_minifier/src/peephole/statement_fusion.rs b/crates/oxc_minifier/src/peephole/statement_fusion.rs index cfea9e725ddbf..55cd11d7e50e7 100644 --- a/crates/oxc_minifier/src/peephole/statement_fusion.rs +++ b/crates/oxc_minifier/src/peephole/statement_fusion.rs @@ -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] diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index 7f91fdf3048c4..c30daf3ddb627 100644 --- a/tasks/minsize/minsize.snap +++ b/tasks/minsize/minsize.snap @@ -5,9 +5,9 @@ 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 @@ -15,13 +15,13 @@ Original | minified | minified | gzip | gzip | Fixture 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