Skip to content

Commit 68703b9

Browse files
committed
feat(minifier): rotate binary expressions to remove parentheses (#15473)
Compress `a | (b | c)` to `a | b | c` and `a * (b % c)` to `b % c * a`.
1 parent 6d4efdd commit 68703b9

File tree

4 files changed

+137
-22
lines changed

4 files changed

+137
-22
lines changed

crates/oxc_minifier/src/peephole/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations {
219219
Self::minimize_binary(expr, ctx);
220220
Self::substitute_loose_equals_undefined(expr, ctx);
221221
Self::substitute_typeof_undefined(expr, ctx);
222+
Self::substitute_rotate_binary_expression(expr, ctx);
222223
}
223224
Expression::UnaryExpression(_) => {
224225
Self::fold_unary_expr(expr, ctx);

crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs

Lines changed: 131 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use oxc_ecmascript::{ToJsString, ToNumber, side_effects::MayHaveSideEffects};
88
use oxc_semantic::ReferenceFlags;
99
use oxc_span::GetSpan;
1010
use oxc_span::SPAN;
11+
use oxc_syntax::precedence::GetPrecedence;
1112
use oxc_syntax::{
1213
number::NumberBase,
1314
operator::{BinaryOperator, UnaryOperator},
@@ -310,6 +311,71 @@ impl<'a> PeepholeOptimizations {
310311
ctx.state.changed = true;
311312
}
312313

314+
/// Rotate associative binary operators:
315+
/// - `a | (b | c)` -> `(a | b) | c`
316+
///
317+
/// Rotate commutative operators to reduce parentheses:
318+
/// - `a * (b % c)` -> `b % c * a`
319+
/// - `a * (b / c)` -> `b / c * a`
320+
pub fn substitute_rotate_binary_expression(expr: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
321+
let Expression::BinaryExpression(e) = expr else { return };
322+
323+
// Handle associative rotation
324+
let is_associative = matches!(
325+
e.operator,
326+
BinaryOperator::BitwiseOR | BinaryOperator::BitwiseAnd | BinaryOperator::BitwiseXOR
327+
);
328+
if is_associative
329+
&& let Expression::BinaryExpression(right) = &e.right
330+
&& right.operator == e.operator
331+
&& !right.right.may_have_side_effects(ctx)
332+
{
333+
let Expression::BinaryExpression(mut right) = e.right.take_in(ctx.ast) else {
334+
return;
335+
};
336+
let mut new_left = ctx.ast.expression_binary(
337+
e.span,
338+
e.left.take_in(ctx.ast),
339+
e.operator,
340+
right.left.take_in(ctx.ast),
341+
);
342+
Self::substitute_rotate_binary_expression(&mut new_left, ctx);
343+
*expr = ctx.ast.expression_binary(
344+
e.span,
345+
new_left,
346+
e.operator,
347+
right.right.take_in(ctx.ast),
348+
);
349+
ctx.state.changed = true;
350+
return;
351+
}
352+
353+
// Handle commutative rotation
354+
if let Expression::BinaryExpression(right) = &e.right
355+
&& matches!(e.operator, BinaryOperator::Multiplication)
356+
&& e.operator.precedence() == right.operator.precedence()
357+
{
358+
// Don't swap if left does not need a parentheses
359+
if let Expression::BinaryExpression(left) = &e.left
360+
&& e.operator.precedence() <= left.operator.precedence()
361+
{
362+
return;
363+
}
364+
365+
// Don't swap if any of the value may have side effects as they may update the other values
366+
if !e.left.may_have_side_effects(ctx)
367+
&& !right.left.may_have_side_effects(ctx)
368+
&& !right.right.may_have_side_effects(ctx)
369+
{
370+
let left = e.left.take_in(ctx.ast);
371+
let right = e.right.take_in(ctx.ast);
372+
e.right = left;
373+
e.left = right;
374+
ctx.state.changed = true;
375+
}
376+
}
377+
}
378+
313379
/// Compress `typeof foo === 'object' && foo !== null` into `typeof foo == 'object' && !!foo`.
314380
///
315381
/// - `typeof foo === 'object' && foo !== null` => `typeof foo == 'object' && !!foo`
@@ -1910,25 +1976,73 @@ mod test {
19101976
test_same("(f.bind(a))(b)");
19111977
}
19121978

1913-
// FIXME: the cases commented out can be implemented
19141979
#[test]
19151980
fn test_rotate_associative_operators() {
1916-
test("a || (b || c)", "(a || b) || c");
1917-
// float multiplication is not always associative
1918-
// <https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-numeric-types-number-multiply>
1919-
test_same("a * (b * c)");
1920-
// test("a | (b | c)", "(a | b) | c");
1921-
test_same("a % (b % c)");
1922-
test_same("a / (b / c)");
1923-
test_same("a - (b - c);");
1924-
// test("a * (b % c);", "b % c * a");
1925-
// test("a * (b / c);", "b / c * a");
1926-
// cannot transform to `c / d * a * b`
1927-
test_same("a * b * (c / d)");
1928-
// test("(a + b) * (c % d)", "c % d * (a + b)");
1929-
test_same("(a / b) * (c % d)");
1930-
test_same("(c = 5) * (c % d)");
1931-
// test("!a * c * (d % e)", "d % e * c * !a");
1981+
test(
1982+
"function f(a, b, c) { return a || (b || c) }",
1983+
"function f(a, b, c) { return (a || b) || c }",
1984+
);
1985+
test(
1986+
"function f(a, b, c) { return a && (b && c) }",
1987+
"function f(a, b, c) { return (a && b) && c }",
1988+
);
1989+
test(
1990+
"function f(a, b, c) { return a ?? (b ?? c) }",
1991+
"function f(a, b, c) { return (a ?? b) ?? c }",
1992+
);
1993+
1994+
test(
1995+
"function f(a, b, c) { return a | (b | c) }",
1996+
"function f(a, b, c) { return (a | b) | c }",
1997+
);
1998+
test(
1999+
"function f(a, b, c) { return a() | (b | c) }",
2000+
"function f(a, b, c) { return (a() | b) | c }",
2001+
);
2002+
test(
2003+
"function f(a, b, c) { return a | (b() | c) }",
2004+
"function f(a, b, c) { return (a | b()) | c }",
2005+
);
2006+
// c() will not be executed when `a | b` throws an error
2007+
test_same("function f(a, b, c) { return a | (b | c()) }");
2008+
test(
2009+
"function f(a, b, c) { return a & (b & c) }",
2010+
"function f(a, b, c) { return (a & b) & c }",
2011+
);
2012+
test(
2013+
"function f(a, b, c) { return a ^ (b ^ c) }",
2014+
"function f(a, b, c) { return (a ^ b) ^ c }",
2015+
);
2016+
2017+
// avoid rotation to prevent precision loss
2018+
// also multiplication is not associative due to floating point precision
2019+
// https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-numeric-types-number-multiply
2020+
test_same("function f(a, b, c) { return a + (b + c) }");
2021+
test_same("function f(a, b, c) { return a - (b - c) }");
2022+
test_same("function f(a, b, c) { return a / (b / c) }");
2023+
test_same("function f(a, b, c) { return a % (b % c) }");
2024+
test_same("function f(a, b, c) { return a ** (b ** c) }");
2025+
2026+
test(
2027+
"function f(a, b, c) { return a * (b % c) }",
2028+
"function f(a, b, c) { return b % c * a }",
2029+
);
2030+
test_same("function f(a, b, c) { return a() * (b % c) }"); // a may update b / c
2031+
test_same("function f(a, b, c) { return a * (b() % c) }"); // b may update b / c
2032+
test_same("function f(a, b, c) { return a * (b % c()) }"); // c may update b / c
2033+
test(
2034+
"function f(a, b, c) { return a * (b / c) }",
2035+
"function f(a, b, c) { return b / c * a }",
2036+
);
2037+
test(
2038+
"function f(a, b, c) { return a * (b * c) }",
2039+
"function f(a, b, c) { return b * c * a }",
2040+
);
2041+
2042+
test_same("function f(a, b, c, d) { return a * b * (c / d) }");
2043+
test_same("function f(a, b, c, d) { return (a + b) * (c % d) }");
2044+
// Don't swap if left has division (already high precedence)
2045+
test_same("function f(a, b, c, d) { return a / b * (c % d) }");
19322046
}
19332047

19342048
#[test]

tasks/minsize/minsize.snap

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ Original | minified | minified | gzip | gzip | Iterations | Fi
1111

1212
544.10 kB | 71.04 kB | 72.48 kB | 25.78 kB | 26.20 kB | 2 | lodash.js
1313

14-
555.77 kB | 267.39 kB | 270.13 kB | 88.01 kB | 90.80 kB | 2 | d3.js
14+
555.77 kB | 267.39 kB | 270.13 kB | 88.00 kB | 90.80 kB | 2 | d3.js
1515

1616
1.01 MB | 439.40 kB | 458.89 kB | 122.06 kB | 126.71 kB | 2 | bundle.min.js
1717

18-
1.25 MB | 642.65 kB | 646.76 kB | 159.39 kB | 163.73 kB | 2 | three.js
18+
1.25 MB | 642.64 kB | 646.76 kB | 159.40 kB | 163.73 kB | 2 | three.js
1919

20-
2.14 MB | 711.15 kB | 724.14 kB | 160.43 kB | 181.07 kB | 2 | victory.js
20+
2.14 MB | 711.13 kB | 724.14 kB | 160.42 kB | 181.07 kB | 2 | victory.js
2121

2222
3.20 MB | 1.00 MB | 1.01 MB | 322.59 kB | 331.56 kB | 3 | echarts.js
2323

tasks/track_memory_allocations/allocs_minifier.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ File | File size || Sys allocs | Sys reallocs |
22
-------------------------------------------------------------------------------------------------------------------------------------------
33
checker.ts | 2.92 MB || 83503 | 14179 || 152592 | 28237
44

5-
cal.com.tsx | 1.06 MB || 40452 | 3033 || 37146 | 4583
5+
cal.com.tsx | 1.06 MB || 40453 | 3033 || 37145 | 4586
66

77
RadixUIAdoptionSection.jsx | 2.52 kB || 85 | 9 || 30 | 6
88

99
pdf.mjs | 567.30 kB || 20494 | 2899 || 47442 | 7725
1010

11-
antd.js | 6.69 MB || 99524 | 13523 || 331573 | 69338
11+
antd.js | 6.69 MB || 99524 | 13523 || 331583 | 69338
1212

1313
binder.ts | 193.08 kB || 4760 | 974 || 7075 | 824
1414

0 commit comments

Comments
 (0)