feat(minifier): fold bitwise operation (#7908)

This PR implements constant evaluation for bitwise operations (`&`, `|`,
`^`).

I wanted to play around with the minifier a bit 🙂
This commit is contained in:
翠 / green 2024-12-15 22:27:05 +09:00 committed by GitHub
parent dcaf674aa8
commit 075bd165a8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 208 additions and 18 deletions

View file

@ -198,9 +198,16 @@ pub trait ConstantEvaluation<'a> {
}
fn eval_binary_expression(&self, e: &BinaryExpression<'a>) -> Option<ConstantValue<'a>> {
let left = &e.left;
let right = &e.right;
match e.operator {
self.eval_binary_operation(e.operator, &e.left, &e.right)
}
fn eval_binary_operation(
&self,
operator: BinaryOperator,
left: &Expression<'a>,
right: &Expression<'a>,
) -> Option<ConstantValue<'a>> {
match operator {
BinaryOperator::Addition => {
if left.may_have_side_effects() || right.may_have_side_effects() {
return None;
@ -230,7 +237,7 @@ pub trait ConstantEvaluation<'a> {
| BinaryOperator::Exponential => {
let lval = self.eval_to_number(left)?;
let rval = self.eval_to_number(right)?;
let val = match e.operator {
let val = match operator {
BinaryOperator::Subtraction => lval - rval,
BinaryOperator::Division => lval / rval,
BinaryOperator::Remainder => {
@ -264,7 +271,7 @@ pub trait ConstantEvaluation<'a> {
let right_val_int = right_val as u32;
let bits = left_val.to_int_32();
let result_val: f64 = match e.operator {
let result_val: f64 = match operator {
BinaryOperator::ShiftLeft => f64::from(bits.wrapping_shl(right_val_int)),
BinaryOperator::ShiftRight => f64::from(bits.wrapping_shr(right_val_int)),
BinaryOperator::ShiftRightZeroFill => {
@ -310,6 +317,34 @@ pub trait ConstantEvaluation<'a> {
_ => unreachable!(),
})
}
BinaryOperator::BitwiseAnd | BinaryOperator::BitwiseOR | BinaryOperator::BitwiseXOR => {
let left_num = self.get_side_free_number_value(left);
let right_num = self.get_side_free_number_value(right);
if let (Some(left_val), Some(right_val)) = (left_num, right_num) {
let left_val_int = left_val.to_int_32();
let right_val_int = right_val.to_int_32();
let result_val: f64 = match operator {
BinaryOperator::BitwiseAnd => f64::from(left_val_int & right_val_int),
BinaryOperator::BitwiseOR => f64::from(left_val_int | right_val_int),
BinaryOperator::BitwiseXOR => f64::from(left_val_int ^ right_val_int),
_ => unreachable!(),
};
return Some(ConstantValue::Number(result_val));
}
let left_bitint = self.get_side_free_bigint_value(left);
let right_bitint = self.get_side_free_bigint_value(right);
if let (Some(left_val), Some(right_val)) = (left_bitint, right_bitint) {
let result_val: BigInt = match operator {
BinaryOperator::BitwiseAnd => left_val & right_val,
BinaryOperator::BitwiseOR => left_val | right_val,
BinaryOperator::BitwiseXOR => left_val ^ right_val,
_ => unreachable!(),
};
return Some(ConstantValue::BigInt(result_val));
}
None
}
_ => None,
}
}

View file

@ -235,19 +235,47 @@ impl<'a, 'b> PeepholeFoldConstants {
ctx.eval_binary_expression(e).map(|v| ctx.value_to_expr(e.span, v))
}
BinaryOperator::BitwiseAnd | BinaryOperator::BitwiseOR | BinaryOperator::BitwiseXOR => {
// TODO:
// self.try_fold_arithmetic_op(e.span, &e.left, &e.right, ctx)
// if (result != subtree) {
// return result;
// }
// return tryFoldLeftChildOp(subtree, left, right);
None
if let Some(v) = ctx.eval_binary_expression(e) {
return Some(ctx.value_to_expr(e.span, v));
}
Self::try_fold_left_child_op(e, ctx)
}
op if op.is_equality() || op.is_compare() => Self::try_fold_comparison(e, ctx),
_ => None,
}
}
fn try_fold_left_child_op(
e: &mut BinaryExpression<'a>,
ctx: Ctx<'a, '_>,
) -> Option<Expression<'a>> {
let op = e.operator;
debug_assert!(matches!(
op,
BinaryOperator::BitwiseAnd | BinaryOperator::BitwiseOR | BinaryOperator::BitwiseXOR
));
let Expression::BinaryExpression(left) = &mut e.left else {
return None;
};
let (v, expr_to_move);
if let Some(result) = ctx.eval_binary_operation(op, &left.left, &e.right) {
(v, expr_to_move) = (result, &mut left.right);
} else if let Some(result) = ctx.eval_binary_operation(op, &left.right, &e.right) {
(v, expr_to_move) = (result, &mut left.left);
} else {
return None;
}
Some(ctx.ast.expression_binary(
e.span,
ctx.ast.move_expression(expr_to_move),
op,
ctx.value_to_expr(Span::new(left.right.span().start, e.right.span().end), v),
))
}
fn try_fold_comparison(e: &BinaryExpression<'a>, ctx: Ctx<'a, 'b>) -> Option<Expression<'a>> {
let left = &e.left;
let right = &e.right;
@ -1118,6 +1146,133 @@ mod test {
test_same("void x()");
}
#[test]
fn test_fold_bitwise_op() {
test("x = 1 & 1", "x = 1");
test("x = 1 & 2", "x = 0");
test("x = 3 & 1", "x = 1");
test("x = 3 & 3", "x = 3");
test("x = 1 | 1", "x = 1");
test("x = 1 | 2", "x = 3");
test("x = 3 | 1", "x = 3");
test("x = 3 | 3", "x = 3");
test("x = 1 ^ 1", "x = 0");
test("x = 1 ^ 2", "x = 3");
test("x = 3 ^ 1", "x = 2");
test("x = 3 ^ 3", "x = 0");
test("x = -1 & 0", "x = 0");
test("x = 0 & -1", "x = 0");
test("x = 1 & 4", "x = 0");
test("x = 2 & 3", "x = 2");
// make sure we fold only when we are supposed to -- not when doing so would
// lose information or when it is performed on nonsensical arguments.
test("x = 1 & 1.1", "x = 1");
test("x = 1.1 & 1", "x = 1");
test("x = 1 & 3000000000", "x = 0");
test("x = 3000000000 & 1", "x = 0");
// Try some cases with | as well
test("x = 1 | 4", "x = 5");
test("x = 1 | 3", "x = 3");
test("x = 1 | 1.1", "x = 1");
// test_same("x = 1 | 3e9");
// these cases look strange because bitwise OR converts unsigned numbers to be signed
test("x = 1 | 3000000001", "x = -1294967295");
test("x = 4294967295 | 0", "x = -1");
}
#[test]
fn test_fold_bitwise_op2() {
test("x = y & 1 & 1", "x = y & 1");
test("x = y & 1 & 2", "x = y & 0");
test("x = y & 3 & 1", "x = y & 1");
test("x = 3 & y & 1", "x = y & 1");
test("x = y & 3 & 3", "x = y & 3");
test("x = 3 & y & 3", "x = y & 3");
test("x = y | 1 | 1", "x = y | 1");
test("x = y | 1 | 2", "x = y | 3");
test("x = y | 3 | 1", "x = y | 3");
test("x = 3 | y | 1", "x = y | 3");
test("x = y | 3 | 3", "x = y | 3");
test("x = 3 | y | 3", "x = y | 3");
test("x = y ^ 1 ^ 1", "x = y ^ 0");
test("x = y ^ 1 ^ 2", "x = y ^ 3");
test("x = y ^ 3 ^ 1", "x = y ^ 2");
test("x = 3 ^ y ^ 1", "x = y ^ 2");
test("x = y ^ 3 ^ 3", "x = y ^ 0");
test("x = 3 ^ y ^ 3", "x = y ^ 0");
test("x = Infinity | NaN", "x=0");
test("x = 12 | NaN", "x=12");
}
#[test]
fn test_fold_bitwise_op_with_big_int() {
test("x = 1n & 1n", "x = 1n");
test("x = 1n & 2n", "x = 0n");
test("x = 3n & 1n", "x = 1n");
test("x = 3n & 3n", "x = 3n");
test("x = 1n | 1n", "x = 1n");
test("x = 1n | 2n", "x = 3n");
test("x = 1n | 3n", "x = 3n");
test("x = 3n | 1n", "x = 3n");
test("x = 3n | 3n", "x = 3n");
test("x = 1n | 4n", "x = 5n");
test("x = 1n ^ 1n", "x = 0n");
test("x = 1n ^ 2n", "x = 3n");
test("x = 3n ^ 1n", "x = 2n");
test("x = 3n ^ 3n", "x = 0n");
test("x = -1n & 0n", "x = 0n");
test("x = 0n & -1n", "x = 0n");
test("x = 1n & 4n", "x = 0n");
test("x = 2n & 3n", "x = 2n");
test("x = 1n & 3000000000n", "x = 0n");
test("x = 3000000000n & 1n", "x = 0n");
// bitwise OR does not affect the sign of a bigint
test("x = 1n | 3000000001n", "x = 3000000001n");
test("x = 4294967295n | 0n", "x = 4294967295n");
test("x = y & 1n & 1n", "x = y & 1n");
test("x = y & 1n & 2n", "x = y & 0n");
test("x = y & 3n & 1n", "x = y & 1n");
test("x = 3n & y & 1n", "x = y & 1n");
test("x = y & 3n & 3n", "x = y & 3n");
test("x = 3n & y & 3n", "x = y & 3n");
test("x = y | 1n | 1n", "x = y | 1n");
test("x = y | 1n | 2n", "x = y | 3n");
test("x = y | 3n | 1n", "x = y | 3n");
test("x = 3n | y | 1n", "x = y | 3n");
test("x = y | 3n | 3n", "x = y | 3n");
test("x = 3n | y | 3n", "x = y | 3n");
test("x = y ^ 1n ^ 1n", "x = y ^ 0n");
test("x = y ^ 1n ^ 2n", "x = y ^ 3n");
test("x = y ^ 3n ^ 1n", "x = y ^ 2n");
test("x = 3n ^ y ^ 1n", "x = y ^ 2n");
test("x = y ^ 3n ^ 3n", "x = y ^ 0n");
test("x = 3n ^ y ^ 3n", "x = y ^ 0n");
}
#[test]
fn test_fold_bitwise_op_additional() {
test("x = null & 1", "x = 0");
test("x = (2 ** 31 - 1) | 1", "x = 2147483647");
test("x = (2 ** 31) | 1", "x = -2147483647");
}
#[test]
fn test_fold_bit_shift() {
test("x = 1 << 0", "x=1");

View file

@ -5,23 +5,23 @@ Original | minified | minified | gzip | gzip | Fixture
173.90 kB | 61.61 kB | 59.82 kB | 19.55 kB | 19.33 kB | moment.js
287.63 kB | 92.61 kB | 90.07 kB | 32.27 kB | 31.95 kB | jquery.js
287.63 kB | 92.60 kB | 90.07 kB | 32.26 kB | 31.95 kB | jquery.js
342.15 kB | 121.79 kB | 118.14 kB | 44.59 kB | 44.37 kB | vue.js
544.10 kB | 73.37 kB | 72.48 kB | 26.13 kB | 26.20 kB | lodash.js
555.77 kB | 276.22 kB | 270.13 kB | 91.15 kB | 90.80 kB | d3.js
555.77 kB | 276.15 kB | 270.13 kB | 91.13 kB | 90.80 kB | d3.js
1.01 MB | 467.14 kB | 458.89 kB | 126.74 kB | 126.71 kB | bundle.min.js
1.25 MB | 662.69 kB | 646.76 kB | 164.02 kB | 163.73 kB | three.js
1.25 MB | 662.53 kB | 646.76 kB | 163.97 kB | 163.73 kB | three.js
2.14 MB | 740.94 kB | 724.14 kB | 181.49 kB | 181.07 kB | victory.js
2.14 MB | 740.87 kB | 724.14 kB | 181.46 kB | 181.07 kB | victory.js
3.20 MB | 1.02 MB | 1.01 MB | 332.09 kB | 331.56 kB | echarts.js
3.20 MB | 1.02 MB | 1.01 MB | 332.10 kB | 331.56 kB | echarts.js
6.69 MB | 2.39 MB | 2.31 MB | 496.17 kB | 488.28 kB | antd.js
10.95 MB | 3.56 MB | 3.49 MB | 911.37 kB | 915.50 kB | typescript.js
10.95 MB | 3.55 MB | 3.49 MB | 910.45 kB | 915.50 kB | typescript.js