mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
feat(minifier): fold boolean-literal ternary branches in value contexts (#24110)
## What Fold a ternary with one boolean literal branch into `&&` / `||`, in value positions (call arguments, returns, assignments). | Input | Output | Condition | |---|---|---| | `c ? false : x` | `!c && x` | always safe | | `c ? x : true` | `!c \|\| x` | always safe | | `c ? true : x` | `c \|\| x` | only when `c` is a boolean | | `c ? x : false` | `c && x` | only when `c` is a boolean | We already do this inside `if` tests. This PR does it everywhere else. The first two rows produce the exact same value for any `c`. The last two rows would return `c` itself instead of `true`/`false`, so they need a boolean-typed `c`. ## How New match arms in `minimize_conditional_expression`, next to the existing `a ? true : false` => `!!a` block. The dropped branch must be side-effect free (same check the existing block uses). The fold is skipped when it would make the output longer, e.g. `a || b ? false : c` would need `!(a || b)`, so it stays as is. Equality tests invert for free: `a === b ? false : x` => `a !== b && x`. ## Example ```js // input use(foo() ? false : bar()); use(a === b ? false : compute()); ``` ```js // before use(foo()?!1:bar()),use(a===b?!1:compute()); // after use(!foo()&&bar()),use(a!==b&&compute()); ``` Found by re-minifying oxc's minsize output with `terser -c`: typescript.js alone has ~290 of these. All minsize fixtures stay equal or smaller. Implemented with Claude Code, reviewed and verified by me.
This commit is contained in:
1 parent
e8b50ee716
commit
2d9b0b3c76
4 files changed
+153
-12
No files matched your search
@@ -431,6 +431,84 @@ impl<'a> PeepholeOptimizations {
|
||||
let test = Self::minimize_not(expr.span, test, ctx);
|
||||
return Some(test);
|
||||
}
|
||||
// "c ? false : x" => "!c && x" (exact for any `c`)
|
||||
(Some(false), None)
|
||||
if Self::can_fold_negated_test(&expr.test)
|
||||
&& !Self::logical_operand_needs_parens(
|
||||
&expr.alternate,
|
||||
LogicalOperator::And,
|
||||
) =>
|
||||
{
|
||||
let test = expr.test.take_in(ctx);
|
||||
let test = Self::minimize_not(expr.span, test, ctx);
|
||||
let right = expr.alternate.take_in(ctx);
|
||||
return Some(Self::join_with_left_associative_op(
|
||||
expr.span,
|
||||
LogicalOperator::And,
|
||||
test,
|
||||
right,
|
||||
ctx,
|
||||
));
|
||||
}
|
||||
// "c ? x : true" => "!c || x" (exact for any `c`)
|
||||
(None, Some(true))
|
||||
if Self::can_fold_negated_test(&expr.test)
|
||||
&& !Self::logical_operand_needs_parens(
|
||||
&expr.consequent,
|
||||
LogicalOperator::Or,
|
||||
) =>
|
||||
{
|
||||
let test = expr.test.take_in(ctx);
|
||||
let test = Self::minimize_not(expr.span, test, ctx);
|
||||
let right = expr.consequent.take_in(ctx);
|
||||
return Some(Self::join_with_left_associative_op(
|
||||
expr.span,
|
||||
LogicalOperator::Or,
|
||||
test,
|
||||
right,
|
||||
ctx,
|
||||
));
|
||||
}
|
||||
// "c ? true : x" => "c || x" (only when `c` is boolean-typed; a
|
||||
// non-boolean truthy `c` would be returned instead of `true`)
|
||||
(Some(true), None)
|
||||
if expr.test.value_type(ctx).is_boolean()
|
||||
&& !Self::logical_operand_needs_parens(&expr.test, LogicalOperator::Or)
|
||||
&& !Self::logical_operand_needs_parens(
|
||||
&expr.alternate,
|
||||
LogicalOperator::Or,
|
||||
) =>
|
||||
{
|
||||
let test = expr.test.take_in(ctx);
|
||||
let right = expr.alternate.take_in(ctx);
|
||||
return Some(Self::join_with_left_associative_op(
|
||||
expr.span,
|
||||
LogicalOperator::Or,
|
||||
test,
|
||||
right,
|
||||
ctx,
|
||||
));
|
||||
}
|
||||
// "c ? x : false" => "c && x" (only when `c` is boolean-typed; a
|
||||
// non-boolean falsy `c` would be returned instead of `false`)
|
||||
(None, Some(false))
|
||||
if expr.test.value_type(ctx).is_boolean()
|
||||
&& !Self::logical_operand_needs_parens(&expr.test, LogicalOperator::And)
|
||||
&& !Self::logical_operand_needs_parens(
|
||||
&expr.consequent,
|
||||
LogicalOperator::And,
|
||||
) =>
|
||||
{
|
||||
let test = expr.test.take_in(ctx);
|
||||
let right = expr.consequent.take_in(ctx);
|
||||
return Some(Self::join_with_left_associative_op(
|
||||
expr.span,
|
||||
LogicalOperator::And,
|
||||
test,
|
||||
right,
|
||||
ctx,
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -687,6 +765,39 @@ impl<'a> PeepholeOptimizations {
|
||||
false
|
||||
}
|
||||
|
||||
/// Returns `true` when `minimize_not(test)` negates `test` without wrapping
|
||||
/// it in a `!(...)`, so folding a conditional into `!test && x` / `!test || x`
|
||||
/// does not grow the output. Equality comparisons invert their operator in
|
||||
/// place (`a === b` -> `a !== b`); anything that is not otherwise
|
||||
/// parenthesized as a unary operand negates to a bare `!test`.
|
||||
fn can_fold_negated_test(test: &Expression<'_>) -> bool {
|
||||
if let Expression::BinaryExpression(e) = test {
|
||||
return e.operator.is_equality();
|
||||
}
|
||||
!Self::test_needs_parens(test)
|
||||
}
|
||||
|
||||
/// Returns `true` if `expr` would need parentheses when printed as an operand
|
||||
/// of the logical operator `op` (`&&` or `||`). Used as a size guard before
|
||||
/// folding a conditional into a logical expression.
|
||||
fn logical_operand_needs_parens(expr: &Expression<'_>, op: LogicalOperator) -> bool {
|
||||
match expr {
|
||||
Expression::SequenceExpression(_)
|
||||
| Expression::AssignmentExpression(_)
|
||||
| Expression::YieldExpression(_)
|
||||
| Expression::ArrowFunctionExpression(_)
|
||||
| Expression::ConditionalExpression(_) => true,
|
||||
// `??` cannot be mixed with `&&`/`||` without parens, and `||` needs
|
||||
// parens as an operand of `&&`. `&&` under `||`, and same-operator
|
||||
// nesting (flattened by `join_with_left_associative_op`), do not.
|
||||
Expression::LogicalExpression(e) => {
|
||||
matches!(e.operator, LogicalOperator::Coalesce)
|
||||
|| matches!((e.operator, op), (LogicalOperator::Or, LogicalOperator::And))
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the expression would need parentheses when used as a unary operand.
|
||||
/// Binary and conditional expressions need wrapping; identifiers, calls, unary, etc. do not.
|
||||
fn test_needs_parens(expr: &Expression<'_>) -> bool {
|
||||
|
||||
@@ -105,3 +105,33 @@ fn test_minimize_conditional_numeric() {
|
||||
// "a ? 0 : 1" stays when parens would make it same or longer
|
||||
test("let x = a + b ? 0 : 1", "let x = a + b ? 0 : 1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_minimize_conditional_boolean_value_context() {
|
||||
// Form 1: "c ? false : x" => "!c && x" (exact for any `c`)
|
||||
test("let x = foo() ? false : bar()", "let x = !foo() && bar()");
|
||||
test("let x = num ? false : y", "let x = !num && y");
|
||||
test("foo(a ? false : b)", "foo(!a && b)");
|
||||
// equality tests invert their operator in place via `minimize_not`
|
||||
test("function f() { return a === b ? false : x }", "function f() { return a !== b && x }");
|
||||
|
||||
// Form 2: "c ? x : true" => "!c || x" (exact for any `c`)
|
||||
test("let x = foo() ? bar() : true", "let x = !foo() || bar()");
|
||||
test("let x = a === b ? c : true", "let x = a !== b || c");
|
||||
// "!a ? true : x" first flips to "a ? x : true", then folds via form 2
|
||||
test("let x = !a ? true : b", "let x = !a || b");
|
||||
|
||||
// Form 3: "c ? true : x" => "c || x" only when `c` is boolean-typed
|
||||
test("let x = a === b ? true : c", "let x = a === b || c");
|
||||
// Form 4: "c ? x : false" => "c && x" only when `c` is boolean-typed
|
||||
test("let x = a === b ? c : false", "let x = a === b && c");
|
||||
|
||||
// Negative: forms 3/4 require a boolean-typed test, else the value changes.
|
||||
test("let x = num ? true : y", "let x = num ? !0 : y");
|
||||
test("let x = num ? y : false", "let x = num ? y : !1");
|
||||
|
||||
// Negative: size guard. `!(a || b) && c` is longer than `a || b ? !1 : c`.
|
||||
test("let x = a || b ? false : c", "let x = a || b ? !1 : c");
|
||||
// Negative: `x` needing parens as a logical operand would not save bytes.
|
||||
test("let x = a ? false : b ? c : d", "let x = a ? !1 : b ? c : d");
|
||||
}
|
||||
@@ -5,23 +5,23 @@ Original | minified | minified | gzip | gzip | Iterations | Fi
|
||||
|
||||
173.90 kB | 59.40 kB | 59.82 kB | 19.15 kB | 19.33 kB | 2 | moment.js
|
||||
|
||||
287.63 kB | 89.25 kB | 90.07 kB | 30.91 kB | 31.95 kB | 2 | jquery.js
|
||||
287.63 kB | 89.25 kB | 90.07 kB | 30.90 kB | 31.95 kB | 2 | jquery.js
|
||||
|
||||
342.15 kB | 116.97 kB | 118.14 kB | 43.18 kB | 44.37 kB | 2 | vue.js
|
||||
342.15 kB | 116.97 kB | 118.14 kB | 43.17 kB | 44.37 kB | 2 | vue.js
|
||||
|
||||
544.10 kB | 70.92 kB | 72.48 kB | 25.74 kB | 26.20 kB | 2 | lodash.js
|
||||
544.10 kB | 70.92 kB | 72.48 kB | 25.73 kB | 26.20 kB | 2 | lodash.js
|
||||
|
||||
555.77 kB | 267.40 kB | 270.13 kB | 88.01 kB | 90.80 kB | 2 | d3.js
|
||||
|
||||
1.01 MB | 439.36 kB | 458.89 kB | 122.05 kB | 126.71 kB | 2 | bundle.min.js
|
||||
1.01 MB | 439.34 kB | 458.89 kB | 122.04 kB | 126.71 kB | 2 | bundle.min.js
|
||||
|
||||
1.25 MB | 642.65 kB | 646.76 kB | 159.41 kB | 163.73 kB | 2 | three.js
|
||||
1.25 MB | 642.62 kB | 646.76 kB | 159.40 kB | 163.73 kB | 2 | three.js
|
||||
|
||||
2.14 MB | 711.07 kB | 724.14 kB | 160.41 kB | 181.07 kB | 2 | victory.js
|
||||
2.14 MB | 711.06 kB | 724.14 kB | 160.41 kB | 181.07 kB | 2 | victory.js
|
||||
|
||||
3.20 MB | 1.00 MB | 1.01 MB | 322.61 kB | 331.56 kB | 2 | echarts.js
|
||||
3.20 MB | 1.00 MB | 1.01 MB | 322.60 kB | 331.56 kB | 2 | echarts.js
|
||||
|
||||
6.69 MB | 2.22 MB | 2.31 MB | 458.44 kB | 488.28 kB | 4 | antd.js
|
||||
6.69 MB | 2.22 MB | 2.31 MB | 458.42 kB | 488.28 kB | 4 | antd.js
|
||||
|
||||
10.95 MB | 3.33 MB | 3.49 MB | 853.11 kB | 915.50 kB | 4 | typescript.js
|
||||
10.95 MB | 3.33 MB | 3.49 MB | 853.07 kB | 915.50 kB | 4 | typescript.js
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
File | File size || Sys allocs | Sys reallocs || Arena allocs | Arena reallocs
|
||||
---------------------------------------------------------------------------------------------------------------------------
|
||||
checker.ts | 2.92 MB || 162 | 5 || 152751 | 28253
|
||||
checker.ts | 2.92 MB || 162 | 5 || 152811 | 28253
|
||||
|
||||
App.tsx | 415.34 kB || 72 | 8 || 17010 | 2702
|
||||
App.tsx | 415.34 kB || 72 | 8 || 17028 | 2702
|
||||
|
||||
RadixUIAdoptionSection.jsx | 2.52 kB || 24 | 0 || 32 | 6
|
||||
|
||||
pdf.mjs | 567.30 kB || 2476 | 205 || 47484 | 7742
|
||||
|
||||
antd.js | 6.69 MB || 870 | 56 || 337232 | 69333
|
||||
antd.js | 6.69 MB || 870 | 56 || 337676 | 69333
|
||||
|
||||
binder.ts | 193.08 kB || 41 | 1 || 7083 | 824
|
||||
|
||||
|
||||
Reference in new issue
Block a user