Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ lint = "clippy --workspace --all-targets --all-features"
codecov = "llvm-cov --workspace --ignore-filename-regex tasks"
coverage = "run -p oxc_coverage --profile coverage --"
benchmark = "run -p oxc_benchmark --release --"
minsize = "run -p oxc_minsize --release --"
minsize = "run -p oxc_minsize --profile coverage --"
rule = "run -p rulegen"

# Build oxlint in release mode
Expand Down
150 changes: 132 additions & 18 deletions crates/oxc_minifier/src/ast_passes/statement_fusion.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use oxc_traverse::Traverse;
use oxc_allocator::Vec;
use oxc_ast::ast::*;
use oxc_span::SPAN;
use oxc_traverse::{Traverse, TraverseCtx};

use crate::CompressorPass;

Expand All @@ -11,12 +14,135 @@ pub struct StatementFusion;

impl<'a> CompressorPass<'a> for StatementFusion {}

impl<'a> Traverse<'a> for StatementFusion {}
impl<'a> Traverse<'a> for StatementFusion {
fn exit_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
Self::fuse_statements(&mut program.body, ctx);
}

fn exit_function_body(&mut self, body: &mut FunctionBody<'a>, ctx: &mut TraverseCtx<'a>) {
Self::fuse_statements(&mut body.statements, ctx);
}

fn exit_block_statement(&mut self, block: &mut BlockStatement<'a>, ctx: &mut TraverseCtx<'a>) {
Self::fuse_statements(&mut block.body, ctx);
}
}

impl StatementFusion {
impl<'a> StatementFusion {
pub fn new() -> Self {
Self {}
}

fn fuse_statements(stmts: &mut Vec<'a, Statement<'a>>, ctx: &mut TraverseCtx<'a>) {
if Self::can_fuse_into_one_statement(stmts) {
Self::fuse_into_one_statement(stmts, ctx);
}
}

fn can_fuse_into_one_statement(stmts: &[Statement<'a>]) -> bool {
let len = stmts.len();
if len <= 1 {
return false;
}
if stmts[0..len - 1].iter().any(|s| !matches!(s, Statement::ExpressionStatement(_))) {
return false;
}
Self::is_fusable_control_statement(&stmts[len - 1])
}

fn is_fusable_control_statement(stmt: &Statement<'a>) -> bool {
match stmt {
Statement::ExpressionStatement(_)
| Statement::IfStatement(_)
| Statement::ThrowStatement(_)
| Statement::SwitchStatement(_) => true,
Statement::ReturnStatement(return_stmt) => return_stmt.argument.is_some(),
// Statement::ForStatement(for_stmt) => {
// // Avoid cases where we have for(var x;_;_) { ....
// for_stmt.init.is_none()
// || for_stmt.init.as_ref().is_some_and(ForStatementInit::is_expression)
// }
// Statement::ForInStatement(for_in_stmt) => {
// TODO
// }
Statement::LabeledStatement(labeled_stmt) => {
Self::is_fusable_control_statement(&labeled_stmt.body)
}
// Statement::BlockStatement(_) => {
// TODO
// }
_ => false,
}
}

fn fuse_into_one_statement(stmts: &mut Vec<'a, Statement<'a>>, ctx: &mut TraverseCtx<'a>) {
let len = stmts.len();
let mut expressions = ctx.ast.vec();

for stmt in stmts.iter_mut().take(len - 1) {
match stmt {
Statement::ExpressionStatement(expr_stmt) => {
if let Expression::SequenceExpression(sequence_expr) = &mut expr_stmt.expression
{
expressions.extend(
sequence_expr
.expressions
.iter_mut()
.map(|e| ctx.ast.move_expression(e)),
);
} else {
expressions.push(ctx.ast.move_expression(&mut expr_stmt.expression));
}
*stmt = ctx.ast.statement_empty(SPAN);
}
_ => unreachable!(),
}
}

let last = stmts.last_mut().unwrap();
Self::fuse_expression_into_control_flow_statement(last, expressions, ctx);

*stmts = ctx.ast.vec1(ctx.ast.move_statement(last));
}

fn fuse_expression_into_control_flow_statement(
stmt: &mut Statement<'a>,
exprs: Vec<'a, Expression<'a>>,
ctx: &mut TraverseCtx<'a>,
) {
let mut exprs = exprs;
let expr = match stmt {
Statement::ExpressionStatement(expr_stmt) => &mut expr_stmt.expression,
Statement::IfStatement(if_stmt) => &mut if_stmt.test,
Statement::ThrowStatement(throw_stmt) => &mut throw_stmt.argument,
Statement::SwitchStatement(switch_stmt) => &mut switch_stmt.discriminant,
Statement::ReturnStatement(return_stmt) => return_stmt.argument.as_mut().unwrap(),
// Statement::ForStatement(for_stmt) => {
// if let Some(init) = for_stmt.init.as_mut() {
// init.as_expression_mut().unwrap()
// } else {
// for_stmt.init =
// Some(ctx.ast.for_statement_init_expression(
// ctx.ast.expression_sequence(SPAN, exprs),
// ));
// return;
// }
// }
Statement::LabeledStatement(labeled_stmt) => {
Self::fuse_expression_into_control_flow_statement(
&mut labeled_stmt.body,
exprs,
ctx,
);
return;
}
_ => {
unreachable!("must match with `Self::is_fusable_control_statement`");
}
};
exprs.push(ctx.ast.move_expression(expr));
*expr = ctx.ast.expression_sequence(SPAN, exprs);
}
}

#[cfg(test)]
Expand Down Expand Up @@ -47,7 +173,6 @@ mod test {
}

#[test]
#[ignore]
fn nothing_to_do() {
fuse_same("");
fuse_same("a");
Expand All @@ -56,7 +181,6 @@ mod test {
}

#[test]
#[ignore]
fn fold_block_with_statements() {
fuse("a;b;c", "a,b,c");
fuse("a();b();c();", "a(),b(),c()");
Expand All @@ -66,7 +190,6 @@ mod test {
}

#[test]
#[ignore]
fn fold_block_into_if() {
fuse("a;b;c;if(x){}", "if(a,b,c,x){}");
fuse("a;b;c;if(x,y){}else{}", "if(a,b,c,x,y){}else{}");
Expand All @@ -78,7 +201,6 @@ mod test {
}

#[test]
#[ignore]
fn fold_block_return() {
fuse("a;b;c;return x", "return a,b,c,x");
fuse("a;b;c;return x+y", "return a,b,c,x+y");
Expand All @@ -88,15 +210,13 @@ mod test {
}

#[test]
#[ignore]
fn fold_block_throw() {
fuse("a;b;c;throw x", "throw a,b,c,x");
fuse("a;b;c;throw x+y", "throw a,b,c,x+y");
fuse_same("a;b;c;throw x;a;b;c");
}

#[test]
#[ignore]
fn fold_switch() {
fuse("a;b;c;switch(x){}", "switch(a,b,c,x){}");
}
Expand All @@ -108,7 +228,6 @@ mod test {
}

#[test]
#[ignore]
fn fuse_into_for_in2() {
// This test case causes a parse warning in ES5 strict out, but is a parse error in ES6+ out.
// setAcceptedLanguage(CompilerOptions.LanguageMode.ECMASCRIPT5_STRICT);
Expand All @@ -134,11 +253,10 @@ mod test {
}

#[test]
#[ignore]
fn fuse_into_label() {
fuse("a;b;c;label:for(x in y){}", "label:for(x in a,b,c,y){}");
fuse("a;b;c;label:for(;g;){}", "label:for(a,b,c;g;){}");
fuse("a;b;c;l1:l2:l3:for(;g;){}", "l1:l2:l3:for(a,b,c;g;){}");
// fuse("a;b;c;label:for(x in y){}", "label:for(x in a,b,c,y){}");
// fuse("a;b;c;label:for(;g;){}", "label:for(a,b,c;g;){}");
// fuse("a;b;c;l1:l2:l3:for(;g;){}", "l1:l2:l3:for(a,b,c;g;){}");
fuse_same("a;b;c;label:while(true){}");
}

Expand All @@ -155,13 +273,11 @@ mod test {
}

#[test]
#[ignore]
fn no_fuse_into_while() {
fuse_same("a;b;c;while(x){}");
}

#[test]
#[ignore]
fn no_fuse_into_do() {
fuse_same("a;b;c;do{}while(x)");
}
Expand Down Expand Up @@ -191,13 +307,11 @@ mod test {
}

#[test]
#[ignore]
fn no_global_scope_changes() {
test_same("a,b,c");
}

#[test]
#[ignore]
fn no_function_block_changes() {
test_same("function foo() { a,b,c }");
}
Expand Down
3 changes: 2 additions & 1 deletion crates/oxc_minifier/src/compressor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ impl<'a> Compressor<'a> {
self.fold_constants(program, &mut ctx);
self.minimize_conditions(program, &mut ctx);
self.remove_dead_code(program, &mut ctx);
self.statement_fusion(program, &mut ctx);
// self.statement_fusion(program, &mut ctx);
self.substitute_alternate_syntax(program, &mut ctx);
self.collapse_variable_declarations(program, &mut ctx);
self.exploit_assigns(program, &mut ctx);
Expand Down Expand Up @@ -77,6 +77,7 @@ impl<'a> Compressor<'a> {
PeepholeRemoveDeadCode::new().build(program, ctx);
}

#[allow(unused)]
fn statement_fusion(&self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
StatementFusion::new().build(program, ctx);
}
Expand Down