Skip to content

Commit

Permalink
feat(minifier): merge assign expression in conditional expression (#8345
Browse files Browse the repository at this point in the history
)

compresses `a ? b = 0 : b = 1` into `b = a ? 0 : 1`

This can be done when `b` is an IdentifierReference and the assignment operator is `=`.

In this circumstance, the evaluation of `b = a ? 0 : 1` is:

1. Let lref be ? Evaluation of LeftHandSideExpression. (this does not have a side effect when LeftHandSideExpression is an IdentifierReference)
2. Let rref be ? Evaluation of AssignmentExpression. (ConditionalExpression is evaluated here)
3. Let rval be ? GetValue(rref).
4. Perform ? PutValue(lref, rval).
5. Return rval.

**References**
- [spec of `=`](https://262.ecma-international.org/15.0/index.html#sec-assignment-operators-runtime-semantics-evaluation)
- [spec of `? :`](https://262.ecma-international.org/15.0/index.html#sec-conditional-operator-runtime-semantics-evaluation)
  • Loading branch information
sapphi-red committed Jan 8, 2025
1 parent 5a648bc commit 8d52cd0
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 12 deletions.
20 changes: 19 additions & 1 deletion crates/oxc_ast/src/ast_impl/js.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,18 @@ impl<'a> Expression<'a> {
matches!(self, Expression::FunctionExpression(_) | Expression::ArrowFunctionExpression(_))
}

/// Returns `true` if this [`Expression`] is an anonymous function definition.
/// Note that this includes [`Class`]s.
/// <https://262.ecma-international.org/15.0/#sec-isanonymousfunctiondefinition>
pub fn is_anonymous_function_definition(&self) -> bool {
match self {
Self::ArrowFunctionExpression(_) => true,
Self::FunctionExpression(func) => func.name().is_none(),
Self::ClassExpression(class) => class.name().is_none(),
_ => false,
}
}

/// Returns `true` if this [`Expression`] is a [`CallExpression`].
pub fn is_call_expression(&self) -> bool {
matches!(self, Expression::CallExpression(_))
Expand Down Expand Up @@ -1160,7 +1172,13 @@ impl<'a> ArrowFunctionExpression<'a> {
}
}

impl Class<'_> {
impl<'a> Class<'a> {
/// Returns this [`Class`]'s name, if it has one.
#[inline]
pub fn name(&self) -> Option<Atom<'a>> {
self.id.as_ref().map(|id| id.name.clone())
}

/// `true` if this [`Class`] is an expression.
///
/// For example,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use oxc_ecmascript::{
constant_evaluation::{ConstantEvaluation, ValueType},
ToInt32, ToJsString, ToNumber,
};
use oxc_span::cmp::ContentEq;
use oxc_span::{GetSpan, SPAN};
use oxc_syntax::{
es_target::ESTarget,
Expand Down Expand Up @@ -153,6 +154,9 @@ impl<'a> Traverse<'a> for PeepholeSubstituteAlternateSyntax {
Expression::TemplateLiteral(t) => Self::try_fold_template_literal(t, ctx),
Expression::BinaryExpression(e) => Self::try_fold_loose_equals_undefined(e, ctx)
.or_else(|| Self::try_compress_typeof_undefined(e, ctx)),
Expression::ConditionalExpression(e) => {
Self::try_merge_conditional_expression_inside(e, ctx)
}
Expression::NewExpression(e) => Self::get_fold_constructor_name(&e.callee, ctx)
.and_then(|name| {
Self::try_fold_object_or_array_constructor(e.span, name, &mut e.arguments, ctx)
Expand Down Expand Up @@ -623,6 +627,46 @@ impl<'a, 'b> PeepholeSubstituteAlternateSyntax {
}
}

/// Merge `consequent` and `alternate` of `ConditionalExpression` inside.
///
/// - `x ? a = 0 : a = 1` -> `a = x ? 0 : 1`
fn try_merge_conditional_expression_inside(
expr: &mut ConditionalExpression<'a>,
ctx: Ctx<'a, 'b>,
) -> Option<Expression<'a>> {
let (
Expression::AssignmentExpression(consequent),
Expression::AssignmentExpression(alternate),
) = (&mut expr.consequent, &mut expr.alternate)
else {
return None;
};
if !matches!(consequent.left, AssignmentTarget::AssignmentTargetIdentifier(_)) {
return None;
}
if consequent.right.is_anonymous_function_definition() {
return None;
}
if consequent.operator != AssignmentOperator::Assign
|| consequent.operator != alternate.operator
|| consequent.left.content_ne(&alternate.left)
{
return None;
}

Some(ctx.ast.expression_assignment(
SPAN,
consequent.operator,
ctx.ast.move_assignment_target(&mut alternate.left),
ctx.ast.expression_conditional(
SPAN,
ctx.ast.move_expression(&mut expr.test),
ctx.ast.move_expression(&mut consequent.right),
ctx.ast.move_expression(&mut alternate.right),
),
))
}

/// Fold `Boolean`, `Number`, `String`, `BigInt` constructors.
///
/// `Boolean(a)` -> `!!a`
Expand Down Expand Up @@ -1125,6 +1169,28 @@ mod test {
test_same("x += -1");
}

#[test]
fn test_compress_conditional_expression_inside() {
test("x ? a = 0 : a = 1", "a = x ? 0 : 1");
test(
"x ? a = function foo() { return 'a' } : a = function bar() { return 'b' }",
"a = x ? function foo() { return 'a' } : function bar() { return 'b' }",
);

// a.b might have a side effect
test_same("x ? a.b = 0 : a.b = 1");
// `a = x ? () => 'a' : () => 'b'` does not set the name property of the function
test_same("x ? a = () => 'a' : a = () => 'b'");
test_same("x ? a = function () { return 'a' } : a = function () { return 'b' }");
test_same("x ? a = class { foo = 'a' } : a = class { foo = 'b' }");

// for non `=` operators, `GetValue(lref)` is called before `Evaluation of AssignmentExpression`
// so cannot be fold to `a += x ? 0 : 1`
// example case: `(()=>{"use strict"; (console.log("log"), 1) ? a += 0 : a += 1; })()`
test_same("x ? a += 0 : a += 1");
test_same("x ? a &&= 0 : a &&= 1");
}

#[test]
fn test_fold_literal_object_constructors() {
test("x = new Object", "x = ({})");
Expand Down
22 changes: 11 additions & 11 deletions tasks/minsize/minsize.snap
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,25 @@ Original | minified | minified | gzip | gzip | Fixture
-------------------------------------------------------------------------------------
72.14 kB | 23.68 kB | 23.70 kB | 8.61 kB | 8.54 kB | react.development.js

173.90 kB | 59.78 kB | 59.82 kB | 19.42 kB | 19.33 kB | moment.js
173.90 kB | 59.77 kB | 59.82 kB | 19.41 kB | 19.33 kB | moment.js

287.63 kB | 90.04 kB | 90.07 kB | 32.06 kB | 31.95 kB | jquery.js
287.63 kB | 90.02 kB | 90.07 kB | 32.05 kB | 31.95 kB | jquery.js

342.15 kB | 118.14 kB | 118.14 kB | 44.51 kB | 44.37 kB | vue.js
342.15 kB | 118.12 kB | 118.14 kB | 44.51 kB | 44.37 kB | vue.js

544.10 kB | 71.75 kB | 72.48 kB | 26.16 kB | 26.20 kB | lodash.js
544.10 kB | 71.72 kB | 72.48 kB | 26.15 kB | 26.20 kB | lodash.js

555.77 kB | 272.82 kB | 270.13 kB | 90.92 kB | 90.80 kB | d3.js
555.77 kB | 272.81 kB | 270.13 kB | 90.92 kB | 90.80 kB | d3.js

1.01 MB | 460.22 kB | 458.89 kB | 126.83 kB | 126.71 kB | bundle.min.js
1.01 MB | 460.21 kB | 458.89 kB | 126.83 kB | 126.71 kB | bundle.min.js

1.25 MB | 652.56 kB | 646.76 kB | 163.52 kB | 163.73 kB | three.js
1.25 MB | 652.53 kB | 646.76 kB | 163.51 kB | 163.73 kB | three.js

2.14 MB | 726.04 kB | 724.14 kB | 180.15 kB | 181.07 kB | victory.js
2.14 MB | 726 kB | 724.14 kB | 180.13 kB | 181.07 kB | victory.js

3.20 MB | 1.01 MB | 1.01 MB | 331.83 kB | 331.56 kB | echarts.js
3.20 MB | 1.01 MB | 1.01 MB | 331.78 kB | 331.56 kB | echarts.js

6.69 MB | 2.32 MB | 2.31 MB | 492.72 kB | 488.28 kB | antd.js
6.69 MB | 2.32 MB | 2.31 MB | 492.61 kB | 488.28 kB | antd.js

10.95 MB | 3.50 MB | 3.49 MB | 908.28 kB | 915.50 kB | typescript.js
10.95 MB | 3.50 MB | 3.49 MB | 908.24 kB | 915.50 kB | typescript.js

0 comments on commit 8d52cd0

Please sign in to comment.