Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(minifier): fold bitwise operation #7908

Merged
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
45 changes: 40 additions & 5 deletions crates/oxc_ecmascript/src/constant_evaluation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -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,
}
}
Expand Down
169 changes: 162 additions & 7 deletions crates/oxc_minifier/src/ast_passes/peephole_fold_constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
Expand Down
12 changes: 6 additions & 6 deletions tasks/minsize/minsize.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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

Loading