mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
feat(minifier): add new IsTerminated trait and use it in exit-point optimization (#24950)
Added a new IsTerminated trait and wired it into handle_if_statement in minimize-exit-points. before this, we only treated direct jump statements as terminating. Now we also handle nested terminated branches, so cases like nested if/else termination optimize correctly. for now this is only used in this one optimization path, but we should reuse IsTerminated in more places (e.g. switch-case and other exit-point/termination optimizations).
This commit is contained in:
1 parent
ae1a39b71c
commit
b31713a693
7 files changed
+123
-102
No files matched your search
@@ -0,0 +1,69 @@
|
||||
use oxc_allocator::ArenaVec;
|
||||
use oxc_ast::ast::{BlockStatement, IfStatement, Statement, TryStatement};
|
||||
|
||||
/// A statement that never completes normally — a direct jump, a kept block
|
||||
/// ending in a jump, an if/else where every branch jumps, or a try/catch where
|
||||
/// every path jumps — makes the rest of the list unreachable.
|
||||
pub trait IsTerminated {
|
||||
fn is_terminated(&self) -> bool;
|
||||
}
|
||||
|
||||
impl IsTerminated for Statement<'_> {
|
||||
fn is_terminated(&self) -> bool {
|
||||
match self {
|
||||
Statement::IfStatement(stmt) => stmt.is_terminated(),
|
||||
Statement::BlockStatement(stmt) => stmt.is_terminated(),
|
||||
Statement::TryStatement(stmt) => stmt.is_terminated(),
|
||||
_ => self.is_jump_statement(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IsTerminated for IfStatement<'_> {
|
||||
fn is_terminated(&self) -> bool {
|
||||
self.consequent.is_terminated() && self.alternate.is_terminated()
|
||||
}
|
||||
}
|
||||
|
||||
impl IsTerminated for TryStatement<'_> {
|
||||
fn is_terminated(&self) -> bool {
|
||||
// A finalizer that aborts overrides however the other blocks complete.
|
||||
// Otherwise the try block must abort, and so must the catch block when
|
||||
// present (an exception thrown before the try block's jump lands there).
|
||||
self.finalizer.as_ref().is_some_and(|f| f.is_terminated())
|
||||
|| (self.block.is_terminated()
|
||||
&& self.handler.as_ref().is_none_or(|h| h.body.is_terminated()))
|
||||
}
|
||||
}
|
||||
|
||||
impl IsTerminated for BlockStatement<'_> {
|
||||
fn is_terminated(&self) -> bool {
|
||||
self.body.is_terminated()
|
||||
}
|
||||
}
|
||||
|
||||
impl IsTerminated for ArenaVec<'_, Statement<'_>> {
|
||||
fn is_terminated(&self) -> bool {
|
||||
// A minimized dead zone keeps only hoisting survivors after the jump:
|
||||
// `function` declarations and the initializer-less `var` stub
|
||||
// re-emitted by `KeepVar`. Their bindings initialize at scope entry
|
||||
// and nothing after an aborting statement ever runs, so skip them
|
||||
// from the back before testing how the block terminates.
|
||||
self.iter()
|
||||
.rev()
|
||||
.find(|stmt| match stmt {
|
||||
Statement::FunctionDeclaration(_) => false,
|
||||
Statement::VariableDeclaration(decl) => {
|
||||
!(decl.kind.is_var() && decl.declarations.iter().all(|d| d.init.is_none()))
|
||||
}
|
||||
_ => true,
|
||||
})
|
||||
.is_some_and(Statement::is_terminated)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IsTerminated> IsTerminated for Option<T> {
|
||||
fn is_terminated(&self) -> bool {
|
||||
self.as_ref().is_some_and(IsTerminated::is_terminated)
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,7 @@
|
||||
mod compression_pass;
|
||||
mod compressor;
|
||||
pub(crate) mod generated;
|
||||
mod is_terminated;
|
||||
mod keep_var;
|
||||
mod minifier_traverse;
|
||||
mod options;
|
||||
|
||||
@@ -11,7 +11,7 @@ use oxc_ecmascript::{
|
||||
use oxc_semantic::ScopeFlags;
|
||||
use oxc_span::{ContentEq, GetSpan, GetSpanMut, SPAN};
|
||||
|
||||
use crate::{TraverseCtx, keep_var::KeepVar};
|
||||
use crate::{TraverseCtx, is_terminated::IsTerminated, keep_var::KeepVar};
|
||||
|
||||
use super::PeepholeOptimizations;
|
||||
|
||||
@@ -80,9 +80,7 @@ impl<'a> PeepholeOptimizations {
|
||||
// kept block ending in a jump, an if/else or try/catch where
|
||||
// every branch jumps — makes the rest of the list unreachable.
|
||||
// https://github.com/rolldown/rolldown/issues/10184
|
||||
if !is_control_flow_dead
|
||||
&& stmts.last().is_some_and(Self::statement_never_completes_normally)
|
||||
{
|
||||
if !is_control_flow_dead && stmts.last().is_some_and(Statement::is_terminated) {
|
||||
is_control_flow_dead = true;
|
||||
}
|
||||
}
|
||||
@@ -435,65 +433,6 @@ impl<'a> PeepholeOptimizations {
|
||||
false
|
||||
}
|
||||
|
||||
/// Whether control never falls through to the statement after this one in
|
||||
/// the same statement list (terser's `aborts`).
|
||||
///
|
||||
/// Any abrupt completion qualifies, whatever its target: a `break` or
|
||||
/// `continue` transfers control past the end of the enclosing statement
|
||||
/// list, never to the next statement in it.
|
||||
///
|
||||
/// Conservative on purpose:
|
||||
/// - A labeled statement completes normally when its body breaks out of
|
||||
/// its own label (`a: { break a; }`), so it never counts.
|
||||
/// - Loops and switches count as completing normally; `while (true)`
|
||||
/// without `break` is handled by loop-specific folds instead.
|
||||
fn statement_never_completes_normally(stmt: &Statement<'a>) -> bool {
|
||||
match stmt {
|
||||
Statement::ReturnStatement(_)
|
||||
| Statement::ThrowStatement(_)
|
||||
| Statement::BreakStatement(_)
|
||||
| Statement::ContinueStatement(_) => true,
|
||||
Statement::BlockStatement(block) => Self::block_never_completes_normally(block),
|
||||
Statement::IfStatement(if_stmt) => {
|
||||
if_stmt.alternate.as_ref().is_some_and(Self::statement_never_completes_normally)
|
||||
&& Self::statement_never_completes_normally(&if_stmt.consequent)
|
||||
}
|
||||
Statement::TryStatement(try_stmt) => {
|
||||
// A finalizer that aborts overrides however the other
|
||||
// blocks complete. Otherwise the try block must abort, and
|
||||
// so must the catch block when present (an exception
|
||||
// thrown before the try block's jump lands there).
|
||||
try_stmt.finalizer.as_ref().is_some_and(|f| Self::block_never_completes_normally(f))
|
||||
|| (Self::block_never_completes_normally(&try_stmt.block)
|
||||
&& try_stmt
|
||||
.handler
|
||||
.as_ref()
|
||||
.is_none_or(|h| Self::block_never_completes_normally(&h.body)))
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn block_never_completes_normally(block: &BlockStatement<'a>) -> bool {
|
||||
// A minimized dead zone keeps only hoisting survivors after the jump:
|
||||
// `function` declarations and the initializer-less `var` stub
|
||||
// re-emitted by `KeepVar`. Their bindings initialize at scope entry
|
||||
// and nothing after an aborting statement ever runs, so skip them
|
||||
// from the back before testing how the block completes.
|
||||
block
|
||||
.body
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|stmt| match stmt {
|
||||
Statement::FunctionDeclaration(_) => false,
|
||||
Statement::VariableDeclaration(decl) => {
|
||||
!(decl.kind.is_var() && decl.declarations.iter().all(|d| d.init.is_none()))
|
||||
}
|
||||
_ => true,
|
||||
})
|
||||
.is_some_and(Self::statement_never_completes_normally)
|
||||
}
|
||||
|
||||
/// For variable declarations:
|
||||
/// * merge with the previous variable declarator if their kinds are the same
|
||||
/// * remove the variable declarator if it is unused
|
||||
@@ -1003,27 +942,27 @@ impl<'a> PeepholeOptimizations {
|
||||
return ControlFlow::Break(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if if_stmt.alternate.is_some() {
|
||||
// "if (a) return b; else if (c) return d; else return e;" => "if (a) return b; if (c) return d; return e;"
|
||||
result.push(Statement::IfStatement(if_stmt));
|
||||
loop {
|
||||
if let Some(Statement::IfStatement(if_stmt)) = result.last_mut()
|
||||
&& if_stmt.consequent.is_jump_statement()
|
||||
&& let Some(stmt) = if_stmt.alternate.take()
|
||||
{
|
||||
if let Statement::BlockStatement(block_stmt) = stmt {
|
||||
Self::handle_block(result, block_stmt, ctx);
|
||||
} else {
|
||||
result.push(stmt);
|
||||
ctx.notice_change();
|
||||
}
|
||||
continue;
|
||||
if if_stmt.alternate.is_some() && if_stmt.consequent.is_terminated() {
|
||||
// "if (a) return b; else if (c) return d; else return e;" => "if (a) return b; if (c) return d; return e;"
|
||||
result.push(Statement::IfStatement(if_stmt));
|
||||
loop {
|
||||
if let Some(Statement::IfStatement(if_stmt)) = result.last_mut()
|
||||
&& if_stmt.consequent.is_terminated()
|
||||
&& let Some(stmt) = if_stmt.alternate.take()
|
||||
{
|
||||
if let Statement::BlockStatement(block_stmt) = stmt {
|
||||
Self::handle_block(result, block_stmt, ctx);
|
||||
} else {
|
||||
result.push(stmt);
|
||||
ctx.notice_change();
|
||||
}
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
return ControlFlow::Continue(());
|
||||
break;
|
||||
}
|
||||
return ControlFlow::Continue(());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,14 @@ fn test_function_return_optimization() {
|
||||
"function f(){if(a()){b()}else{c();return;}}",
|
||||
"function f(){if(a())b();else{c();return}}",
|
||||
); // function f(){a()?b():c()}
|
||||
test(
|
||||
"function f(){ if(a()) { if (a) { return } throw a; } else return 2; }",
|
||||
"function f(){ if(a()) { if (a) return; throw a } return 2 }",
|
||||
);
|
||||
test(
|
||||
"function f(){if(a()){if(b()){d();return;}else{return;}}else{return;} c();}",
|
||||
"function f(){if(a()){if(b()){d();return}return}}",
|
||||
); // function f(){a()&&b()&&d()}
|
||||
test("function f(){if(a()){b();return;}else;}", "function f(){if(a()){b();return}}"); // function f(){a()&&b()}
|
||||
test("function f(){if(a()){return;}else{return;} return;}", "function f(){a();}");
|
||||
test("function f(){if(a()){return;}else{return;} b();}", "function f(){a()}");
|
||||
@@ -108,6 +116,10 @@ fn test_while_continue_optimization() {
|
||||
test("while(true){if(a()){b();continue;}else;}", "for (;;) if(a()){b();continue;}"); // for(;;)a()&&b();
|
||||
test("while(true){if(a()){continue;}else{continue;} continue;}", "for(;;)a();");
|
||||
test("while(true){if(a()){continue;}else{continue;} b();}", "for(;;)a();");
|
||||
test(
|
||||
"while(true){d();if(a()){continue;}else if(b()){c();continue;}else{continue;}}",
|
||||
"for(;;)if(d(),!a()&&b()){c();continue}",
|
||||
); // for(;;)d(),!a()&&b()&&c();
|
||||
|
||||
test("while(true)while(a())continue;", "for(;;)for(;a();)continue;"); // for(;;)for(;a(););
|
||||
test("while(true)for(x in a())continue", "for(;;)for(x in a())continue;"); // for(;;)for(x in a());
|
||||
|
||||
@@ -184,7 +184,7 @@ fn test_handle_switch_statement() {
|
||||
); // a === 3 && b && c();
|
||||
test(
|
||||
"switch (a) { case 3: { if(b) {c(); break;} else { d(); break;} }}",
|
||||
"switch (a) { case 3: if(b) {c(); break;} else { d(); break;} }",
|
||||
"switch (a) { case 3: if(b) {c(); break;} d(); }",
|
||||
); // if (a === 3) b ? c() : d();
|
||||
test("switch (a) { case 3: { for (;;) break } }", "if (a === 3) for (;;) break;");
|
||||
test("switch (a) { case 3: { for (b of c) break; } }", "if (a === 3) for (b of c) break;");
|
||||
|
||||
@@ -3,25 +3,25 @@ Original | minified | minified | gzip | gzip | Iterations | Fi
|
||||
-------------------------------------------------------------------------------------
|
||||
72.14 kB | 23.11 kB | 23.70 kB | 8.34 kB | 8.54 kB | 2 | react.development.js
|
||||
|
||||
173.90 kB | 59.35 kB | 59.82 kB | 19.15 kB | 19.33 kB | 2 | moment.js
|
||||
173.90 kB | 59.34 kB | 59.82 kB | 19.15 kB | 19.33 kB | 2 | moment.js
|
||||
|
||||
287.63 kB | 89.24 kB | 90.07 kB | 30.91 kB | 31.95 kB | 2 | jquery.js
|
||||
287.63 kB | 89.19 kB | 90.07 kB | 30.90 kB | 31.95 kB | 2 | jquery.js
|
||||
|
||||
342.15 kB | 116.98 kB | 118.14 kB | 43.19 kB | 44.37 kB | 2 | vue.js
|
||||
342.15 kB | 116.92 kB | 118.14 kB | 43.17 kB | 44.37 kB | 2 | vue.js
|
||||
|
||||
544.10 kB | 70.91 kB | 72.48 kB | 25.73 kB | 26.20 kB | 2 | lodash.js
|
||||
|
||||
555.77 kB | 267.20 kB | 270.13 kB | 87.99 kB | 90.80 kB | 2 | d3.js
|
||||
555.77 kB | 267.19 kB | 270.13 kB | 87.99 kB | 90.80 kB | 2 | d3.js
|
||||
|
||||
1.01 MB | 439.26 kB | 458.89 kB | 122.03 kB | 126.71 kB | 2 | bundle.min.js
|
||||
1.01 MB | 439.20 kB | 458.89 kB | 122.02 kB | 126.71 kB | 2 | bundle.min.js
|
||||
|
||||
1.25 MB | 642.44 kB | 646.76 kB | 159.37 kB | 163.73 kB | 2 | three.js
|
||||
1.25 MB | 642.41 kB | 646.76 kB | 159.36 kB | 163.73 kB | 2 | three.js
|
||||
|
||||
2.14 MB | 710.90 kB | 724.14 kB | 160.38 kB | 181.07 kB | 2 | victory.js
|
||||
2.14 MB | 710.74 kB | 724.14 kB | 160.33 kB | 181.07 kB | 2 | victory.js
|
||||
|
||||
3.20 MB | 1.00 MB | 1.01 MB | 322.56 kB | 331.56 kB | 2 | echarts.js
|
||||
3.20 MB | 1.00 MB | 1.01 MB | 322.55 kB | 331.56 kB | 2 | echarts.js
|
||||
|
||||
6.69 MB | 2.12 MB | 2.31 MB | 453.80 kB | 488.28 kB | 5 | antd.js
|
||||
|
||||
10.95 MB | 3.33 MB | 3.49 MB | 852.84 kB | 915.50 kB | 4 | typescript.js
|
||||
10.95 MB | 3.33 MB | 3.49 MB | 852.67 kB | 915.50 kB | 4 | typescript.js
|
||||
|
||||
@@ -5,9 +5,9 @@ checker.ts:
|
||||
sys deallocs: 152
|
||||
sys alloc bytes: 15030872 # 15.03 MB
|
||||
sys peak growth: 10203896 # 10.20 MB
|
||||
arena allocs: 152956
|
||||
arena reallocs: 28386
|
||||
arena size: 19332672 # 19.33 MB
|
||||
arena allocs: 153112
|
||||
arena reallocs: 28468
|
||||
arena size: 19341336 # 19.34 MB
|
||||
|
||||
App.tsx:
|
||||
file size: 415342 # 415.34 kB
|
||||
@@ -16,8 +16,8 @@ App.tsx:
|
||||
sys deallocs: 63
|
||||
sys alloc bytes: 2136669 # 2.14 MB
|
||||
sys peak growth: 1620605 # 1.62 MB
|
||||
arena allocs: 17035
|
||||
arena reallocs: 2714
|
||||
arena allocs: 17049
|
||||
arena reallocs: 2721
|
||||
arena size: 2916824 # 2.92 MB
|
||||
|
||||
RadixUIAdoptionSection.jsx:
|
||||
@@ -38,8 +38,8 @@ pdf.mjs:
|
||||
sys deallocs: 2467
|
||||
sys alloc bytes: 5348219 # 5.35 MB
|
||||
sys peak growth: 3707391 # 3.71 MB
|
||||
arena allocs: 47436
|
||||
arena reallocs: 7715
|
||||
arena allocs: 47442
|
||||
arena reallocs: 7718
|
||||
arena size: 6374320 # 6.37 MB
|
||||
|
||||
antd.js:
|
||||
@@ -49,9 +49,9 @@ antd.js:
|
||||
sys deallocs: 1038
|
||||
sys alloc bytes: 29975903 # 29.98 MB
|
||||
sys peak growth: 19934725 # 19.93 MB
|
||||
arena allocs: 371560
|
||||
arena reallocs: 76271
|
||||
arena size: 49106184 # 49.11 MB
|
||||
arena allocs: 371599
|
||||
arena reallocs: 76289
|
||||
arena size: 49108736 # 49.11 MB
|
||||
|
||||
binder.ts:
|
||||
file size: 193077 # 193.08 kB
|
||||
@@ -60,8 +60,8 @@ binder.ts:
|
||||
sys deallocs: 33
|
||||
sys alloc bytes: 963614 # 963.61 kB
|
||||
sys peak growth: 658042 # 658.04 kB
|
||||
arena allocs: 7078
|
||||
arena reallocs: 841
|
||||
arena allocs: 7079
|
||||
arena reallocs: 842
|
||||
arena size: 1169840 # 1.17 MB
|
||||
|
||||
kitchen-sink.tsx:
|
||||
|
||||
Reference in new issue
Block a user