From 4ea197e174551a63bb10d7b5816ba12212d5c1d1 Mon Sep 17 00:00:00 2001 From: Boshen Date: Sun, 5 Jan 2025 20:30:38 +0800 Subject: [PATCH] feat(minifier): improve constant evaluation --- .../src/constant_evaluation/mod.rs | 105 ++++++--------- .../src/constant_evaluation/value_type.rs | 24 +++- .../src/ast_passes/peephole_fold_constants.rs | 123 +++++++++--------- .../peephole_minimize_conditions.rs | 3 + .../peephole_replace_known_methods.rs | 51 ++++---- tasks/minsize/minsize.snap | 2 +- 6 files changed, 150 insertions(+), 158 deletions(-) diff --git a/crates/oxc_ecmascript/src/constant_evaluation/mod.rs b/crates/oxc_ecmascript/src/constant_evaluation/mod.rs index 101ac990805472..b9d04e3e51ce5e 100644 --- a/crates/oxc_ecmascript/src/constant_evaluation/mod.rs +++ b/crates/oxc_ecmascript/src/constant_evaluation/mod.rs @@ -1,3 +1,4 @@ +use core::f64; use std::{borrow::Cow, cmp::Ordering}; use num_bigint::BigInt; @@ -149,6 +150,9 @@ pub trait ConstantEvaluation<'a> { UnaryOperator::Void => Some(f64::NAN), _ => None, }, + Expression::SequenceExpression(s) => { + s.expressions.last().and_then(|e| self.eval_to_number(e)) + } expr => { use crate::ToNumber; expr.to_number() @@ -247,39 +251,20 @@ pub trait ConstantEvaluation<'a> { }; Some(ConstantValue::Number(val)) } - #[expect(clippy::cast_sign_loss, clippy::cast_possible_truncation)] + #[expect(clippy::cast_sign_loss)] BinaryOperator::ShiftLeft | BinaryOperator::ShiftRight | BinaryOperator::ShiftRightZeroFill => { - 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) { - if left_val.fract() != 0.0 || right_val.fract() != 0.0 { - return None; - } - // only the lower 5 bits are used when shifting, so don't do anything - // if the shift amount is outside [0,32) - if !(0.0..32.0).contains(&right_val) { - return None; - } - let right_val_int = right_val as u32; - let bits = left_val.to_int_32(); - - 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 => { - // JavaScript always treats the result of >>> as unsigned. - // We must force Rust to do the same here. - let bits = bits as u32; - let res = bits.wrapping_shr(right_val_int); - f64::from(res) - } - _ => unreachable!(), - }; - return Some(ConstantValue::Number(result_val)); - } - None + let left = self.get_side_free_number_value(left)?; + let right = self.get_side_free_number_value(right)?; + let left = left.to_int_32(); + let right = (right.to_int_32() as u32) & 31; + Some(ConstantValue::Number(match operator { + BinaryOperator::ShiftLeft => f64::from(left << right), + BinaryOperator::ShiftRight => f64::from(left >> right), + BinaryOperator::ShiftRightZeroFill => f64::from((left as u32) >> right), + _ => unreachable!(), + })) } BinaryOperator::LessThan => { self.is_less_than(left, right, true).map(|value| match value { @@ -401,52 +386,36 @@ pub trait ConstantEvaluation<'a> { }; Some(ConstantValue::String(Cow::Borrowed(s))) } - UnaryOperator::Void => { - if (!expr.argument.is_number() || !expr.argument.is_number_0()) - && !expr.may_have_side_effects() - { - return Some(ConstantValue::Undefined); - } - None - } + UnaryOperator::Void => (expr.argument.is_literal() || !expr.may_have_side_effects()) + .then_some(ConstantValue::Undefined), UnaryOperator::LogicalNot => { self.get_boolean_value(&expr.argument).map(|b| !b).map(ConstantValue::Boolean) } UnaryOperator::UnaryPlus => { self.eval_to_number(&expr.argument).map(ConstantValue::Number) } - UnaryOperator::UnaryNegation => { - let ty = ValueType::from(&expr.argument); - match ty { - ValueType::BigInt => { - self.eval_to_big_int(&expr.argument).map(|v| -v).map(ConstantValue::BigInt) - } - ValueType::Number => self - .eval_to_number(&expr.argument) - .map(|v| if v.is_nan() { v } else { -v }) - .map(ConstantValue::Number), - _ => None, + UnaryOperator::UnaryNegation => match ValueType::from(&expr.argument) { + ValueType::BigInt => { + self.eval_to_big_int(&expr.argument).map(|v| -v).map(ConstantValue::BigInt) } - } - UnaryOperator::BitwiseNot => { - let ty = ValueType::from(&expr.argument); - match ty { - ValueType::BigInt => { - self.eval_to_big_int(&expr.argument).map(|v| !v).map(ConstantValue::BigInt) - } - #[expect(clippy::cast_lossless)] - ValueType::Number => self - .eval_to_number(&expr.argument) - .map(|v| !v.to_int_32()) - .map(|v| v as f64) - .map(ConstantValue::Number), - ValueType::Undefined | ValueType::Null => Some(ConstantValue::Number(-1.0)), - ValueType::Boolean => self - .get_side_free_boolean_value(&expr.argument) - .map(|v| ConstantValue::Number(if v { -2.0 } else { -1.0 })), - _ => None, + ValueType::Number => self + .eval_to_number(&expr.argument) + .map(|v| if v.is_nan() { v } else { -v }) + .map(ConstantValue::Number), + ValueType::Undefined => Some(ConstantValue::Number(f64::NAN)), + ValueType::Null => Some(ConstantValue::Number(-0.0)), + _ => None, + }, + UnaryOperator::BitwiseNot => match ValueType::from(&expr.argument) { + ValueType::BigInt => { + self.eval_to_big_int(&expr.argument).map(|v| !v).map(ConstantValue::BigInt) } - } + #[expect(clippy::cast_lossless)] + _ => self + .eval_to_number(&expr.argument) + .map(|v| (!v.to_int_32()) as f64) + .map(ConstantValue::Number), + }, UnaryOperator::Delete => None, } } diff --git a/crates/oxc_ecmascript/src/constant_evaluation/value_type.rs b/crates/oxc_ecmascript/src/constant_evaluation/value_type.rs index 4873bf5e6eb806..d2130876b9433d 100644 --- a/crates/oxc_ecmascript/src/constant_evaluation/value_type.rs +++ b/crates/oxc_ecmascript/src/constant_evaluation/value_type.rs @@ -93,6 +93,7 @@ impl<'a> From<&Expression<'a>> for ValueType { Expression::SequenceExpression(e) => { e.expressions.last().map_or(ValueType::Undetermined, Self::from) } + Expression::AssignmentExpression(e) => Self::from(&e.right), _ => Self::Undetermined, } } @@ -115,8 +116,27 @@ impl<'a> From<&BinaryExpression<'a>> for ValueType { } Self::Undetermined } - BinaryOperator::Instanceof => Self::Boolean, - _ => Self::Undetermined, + BinaryOperator::Subtraction + | BinaryOperator::Multiplication + | BinaryOperator::Division + | BinaryOperator::Remainder + | BinaryOperator::ShiftLeft + | BinaryOperator::BitwiseOR + | BinaryOperator::ShiftRight + | BinaryOperator::BitwiseXOR + | BinaryOperator::BitwiseAnd + | BinaryOperator::Exponential + | BinaryOperator::ShiftRightZeroFill => Self::Number, + BinaryOperator::Instanceof + | BinaryOperator::In + | BinaryOperator::Equality + | BinaryOperator::Inequality + | BinaryOperator::StrictEquality + | BinaryOperator::StrictInequality + | BinaryOperator::LessThan + | BinaryOperator::LessEqualThan + | BinaryOperator::GreaterThan + | BinaryOperator::GreaterEqualThan => Self::Boolean, } } } diff --git a/crates/oxc_minifier/src/ast_passes/peephole_fold_constants.rs b/crates/oxc_minifier/src/ast_passes/peephole_fold_constants.rs index 92dbc677265b1b..165f9eaa6417d1 100644 --- a/crates/oxc_minifier/src/ast_passes/peephole_fold_constants.rs +++ b/crates/oxc_minifier/src/ast_passes/peephole_fold_constants.rs @@ -51,7 +51,7 @@ impl<'a, 'b> PeepholeFoldConstants { Self { changed: false } } - #[allow(clippy::float_cmp)] + #[expect(clippy::float_cmp)] fn try_fold_unary_expr(e: &UnaryExpression<'a>, ctx: Ctx<'a, 'b>) -> Option> { match e.operator { // Do not fold `void 0` back to `undefined`. @@ -217,7 +217,7 @@ impl<'a, 'b> PeepholeFoldConstants { None } - #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + #[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)] fn try_fold_binary_expr( e: &mut BinaryExpression<'a>, ctx: Ctx<'a, 'b>, @@ -300,7 +300,7 @@ impl<'a, 'b> PeepholeFoldConstants { } // https://github.com/evanw/esbuild/blob/v0.24.2/internal/js_ast/js_ast_helpers.go#L1128 - #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + #[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)] #[must_use] fn approximate_printed_int_char_count(value: f64) -> usize { let mut count = if value.is_infinite() { @@ -505,48 +505,37 @@ impl<'a, 'b> PeepholeFoldConstants { ) -> Option { let left = ValueType::from(left_expr); let right = ValueType::from(right_expr); - if left != ValueType::Undetermined && right != ValueType::Undetermined { + if !left.is_undetermined() && !right.is_undetermined() { // Strict equality can only be true for values of the same type. if left != right { return Some(false); } return match left { ValueType::Number => { - let left_number = ctx.get_side_free_number_value(left_expr); - let right_number = ctx.get_side_free_number_value(right_expr); - - if let (Some(l_num), Some(r_num)) = (left_number, right_number) { - if l_num.is_nan() || r_num.is_nan() { - return Some(false); - } - - return Some(l_num == r_num); + let lnum = ctx.get_side_free_number_value(left_expr)?; + let rnum = ctx.get_side_free_number_value(right_expr)?; + if lnum.is_nan() || rnum.is_nan() { + return Some(false); } - - None + Some(lnum == rnum) } ValueType::String => { - let left_string = ctx.get_side_free_string_value(left_expr); - let right_string = ctx.get_side_free_string_value(right_expr); - if let (Some(left_string), Some(right_string)) = (left_string, right_string) { - return Some(left_string == right_string); - } - None + let left = ctx.get_side_free_string_value(left_expr)?; + let right = ctx.get_side_free_string_value(right_expr)?; + Some(left == right) } ValueType::Undefined | ValueType::Null => Some(true), ValueType::Boolean if right.is_boolean() => { - let left = ctx.get_boolean_value(left_expr); - let right = ctx.get_boolean_value(right_expr); - if let (Some(left_bool), Some(right_bool)) = (left, right) { - return Some(left_bool == right_bool); - } - None + let left = ctx.get_boolean_value(left_expr)?; + let right = ctx.get_boolean_value(right_expr)?; + Some(left == right) } - // TODO - ValueType::BigInt - | ValueType::Object - | ValueType::Boolean - | ValueType::Undetermined => None, + ValueType::BigInt => { + let left = ctx.get_side_free_bigint_value(left_expr)?; + let right = ctx.get_side_free_bigint_value(right_expr)?; + Some(left == right) + } + ValueType::Object | ValueType::Boolean | ValueType::Undetermined => None, }; } @@ -648,6 +637,11 @@ mod test { test(source_text, source_text); } + #[test] + fn test_comparison() { + test("(1, 2) !== 2", "false"); + } + #[test] fn undefined_comparison1() { test("undefined == undefined", "true"); @@ -1103,31 +1097,28 @@ mod test { } #[test] - fn unary_ops() { - // TODO: need to port - // These cases are handled by PeepholeRemoveDeadCode in closure-compiler. - // test_same("!foo()"); - // test_same("~foo()"); - // test_same("-foo()"); - - // These cases are handled here. + fn test_fold_unary() { + test_same("!foo()"); + test_same("~foo()"); + test_same("-foo()"); + test("a=!true", "a=false"); test("a=!10", "a=false"); test("a=!false", "a=true"); test_same("a=!foo()"); - // test("a=-0", "a=-0.0"); - // test("a=-(0)", "a=-0.0"); + + test("a=-0", "a=-0"); + test("a=-(0)", "a=-0"); test_same("a=-Infinity"); test("a=-NaN", "a=NaN"); test_same("a=-foo()"); - test("a=~~0", "a=0"); - test("a=~~10", "a=10"); - test("a=~-7", "a=6"); - test_same("a=~~foo()"); + test("-undefined", "NaN"); + test("-null", "-0"); + test("-NaN", "NaN"); - // test("a=+true", "a=1"); + test("a=+true", "a=1"); test("a=+10", "a=10"); - // test("a=+false", "a=0"); + test("a=+false", "a=0"); test_same("a=+foo()"); test_same("a=+f"); // test("a=+(f?true:false)", "a=+(f?1:0)"); @@ -1135,15 +1126,19 @@ mod test { test("a=+Infinity", "a=Infinity"); test("a=+NaN", "a=NaN"); test("a=+-7", "a=-7"); - // test("a=+.5", "a=.5"); + test("a=+.5", "a=.5"); + test("a=~~0", "a=0"); + test("a=~~10", "a=10"); + test("a=~-7", "a=6"); + test_same("a=~~foo()"); test("a=~0xffffffff", "a=0"); test("a=~~0xffffffff", "a=-1"); - // test_same("a=~.5", PeepholeFoldConstants.FRACTIONAL_BITWISE_OPERAND); + // test_same("a=~.5"); } #[test] - fn unary_with_big_int() { + fn test_fold_unary_big_int() { test("-(1n)", "-1n"); test("- -1n", "1n"); test("!1n", "false"); @@ -1453,6 +1448,8 @@ mod test { test("~null", "-1"); test("~false", "-1"); test("~true", "-2"); + test("~'1'", "-2"); + test("~'-1'", "0"); } #[test] @@ -1485,9 +1482,9 @@ mod test { test("x = 0xffffffff << 0", "x=-1"); test("x = 0xffffffff << 4", "x=-16"); - test("1 << 32", "1<<32"); + test("1 << 32", "1"); test("1 << -1", "1<<-1"); - test("1 >> 32", "1>>32"); + test("1 >> 32", "1"); // Regression on #6161, ported from . test("-2147483647 >>> 0", "2147483649"); @@ -1535,6 +1532,8 @@ mod test { // test("x = (p1 + (p2 + 'a')) + 'b'", "x = (p1 + (p2 + 'ab'))"); // test("'a' + ('b' + p1) + 1", "'ab' + p1 + 1"); // test("x = 'a' + ('b' + p1 + 'c')", "x = 'ab' + (p1 + 'c')"); + test("void 0 + ''", "'undefined'"); + test_same("x = 'a' + (4 + p1 + 'a')"); test_same("x = p1 / 3 + 4"); test_same("foo() + 3 + 'a' + foo()"); @@ -1617,18 +1616,24 @@ mod test { test_same("x = null ** 0"); } - #[test] - fn test_fold_shift_right_zero_fill() { - test("10 >>> 1", "5"); - test_same("-1 >>> 0"); - } - #[test] fn test_fold_shift_left() { test("1 << 3", "8"); + test("1.2345 << 0", "1"); test_same("1 << 24"); } + #[test] + fn test_fold_shift_right() { + test("2147483647 >> -32.1", "2147483647"); + } + + #[test] + fn test_fold_shift_right_zero_fill() { + test("10 >>> 1", "5"); + test_same("-1 >>> 0"); + } + #[test] fn test_fold_left() { test_same("(+x - 1) + 2"); // not yet diff --git a/crates/oxc_minifier/src/ast_passes/peephole_minimize_conditions.rs b/crates/oxc_minifier/src/ast_passes/peephole_minimize_conditions.rs index 01f76cf4fa81ae..06539ded24dfc0 100644 --- a/crates/oxc_minifier/src/ast_passes/peephole_minimize_conditions.rs +++ b/crates/oxc_minifier/src/ast_passes/peephole_minimize_conditions.rs @@ -506,6 +506,9 @@ impl<'a> PeepholeMinimizeConditions { true } + // `a instanceof b === true` -> `a instanceof b` + // `a instanceof b === false` -> `!(a instanceof b)` + // ^^^^^^^^^^^^^^ `ValueType::from(&e.left).is_boolean()` is `true`. fn try_minimize_binary( e: &mut BinaryExpression<'a>, ctx: &mut TraverseCtx<'a>, diff --git a/crates/oxc_minifier/src/ast_passes/peephole_replace_known_methods.rs b/crates/oxc_minifier/src/ast_passes/peephole_replace_known_methods.rs index 17eba2fcd6fe6a..7d77f407702be5 100644 --- a/crates/oxc_minifier/src/ast_passes/peephole_replace_known_methods.rs +++ b/crates/oxc_minifier/src/ast_passes/peephole_replace_known_methods.rs @@ -176,10 +176,11 @@ impl<'a> PeepholeReplaceKnownMethods { string_lit: &StringLiteral<'a>, ctx: &mut TraverseCtx<'a>, ) -> Option> { - let char_at_index = call_expr.arguments.first().and_then(|arg| match arg { - Argument::SpreadElement(_) => None, - _ => Ctx(ctx).get_side_free_number_value(arg.to_expression()), - })?; + let char_at_index = match call_expr.arguments.first() { + None => Some(0.0), + Some(Argument::SpreadElement(_)) => None, + Some(e) => Ctx(ctx).get_side_free_number_value(e.to_expression()), + }?; let span = call_expr.span; // TODO: if `result` is `None`, return `NaN` instead of skipping the optimization let result = string_lit.value.as_str().char_code_at(Some(char_at_index))?; @@ -226,26 +227,19 @@ impl<'a> PeepholeReplaceKnownMethods { Some(ctx.ast.expression_string_literal(span, result, None)) } + #[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_lossless)] fn try_fold_string_from_char_code( ce: &CallExpression<'a>, ctx: &mut TraverseCtx<'a>, ) -> Option> { + let ctx = Ctx(ctx); let args = &ce.arguments; - if args.iter().any(|arg| !matches!(arg, Argument::NumericLiteral(_))) { - return None; - } let mut s = String::with_capacity(args.len()); for arg in args { - let Argument::NumericLiteral(lit) = arg else { unreachable!() }; - if lit.value.is_nan() || lit.value.is_infinite() { - return None; - } - let v = lit.value.to_int_32(); - if v >= 65535 { - return None; - } - let Ok(v) = u32::try_from(v) else { return None }; - let Ok(c) = char::try_from(v) else { return None }; + let expr = arg.as_expression()?; + let v = ctx.get_side_free_number_value(expr)?; + let v = v.to_int_32() as u16 as u32; + let c = char::try_from(v).ok()?; s.push(c); } Some(ctx.ast.expression_string_literal(ce.span, s, None)) @@ -508,6 +502,7 @@ mod test { #[test] fn test_fold_string_char_code_at() { + fold("x = 'abcde'.charCodeAt()", "x = 97"); fold("x = 'abcde'.charCodeAt(0)", "x = 97"); fold("x = 'abcde'.charCodeAt(1)", "x = 98"); fold("x = 'abcde'.charCodeAt(2)", "x = 99"); @@ -1099,17 +1094,17 @@ mod test { test("String.fromCharCode(120)", "'x'"); test("String.fromCharCode(120, 121)", "'xy'"); test_same("String.fromCharCode(55358, 56768)"); - test("String.fromCharCode(0x10000)", "String.fromCharCode(65536)"); - test("String.fromCharCode(0x10078, 0x10079)", "String.fromCharCode(0x10078, 0x10079)"); - test("String.fromCharCode(0x1_0000_FFFF)", "String.fromCharCode(4295032831)"); - test_same("String.fromCharCode(NaN)"); - test_same("String.fromCharCode(-Infinity)"); - test_same("String.fromCharCode(Infinity)"); - test_same("String.fromCharCode(null)"); - test_same("String.fromCharCode(undefined)"); - test_same("String.fromCharCode('123')"); + test("String.fromCharCode(0x10000)", "'\\0'"); + test("String.fromCharCode(0x10078, 0x10079)", "'xy'"); + test("String.fromCharCode(0x1_0000_FFFF)", "'\u{ffff}'"); + test("String.fromCharCode(NaN)", "'\\0'"); + test("String.fromCharCode(-Infinity)", "'\\0'"); + test("String.fromCharCode(Infinity)", "'\\0'"); + test("String.fromCharCode(null)", "'\\0'"); + test("String.fromCharCode(undefined)", "'\\0'"); + test("String.fromCharCode('123')", "'{'"); test_same("String.fromCharCode(x)"); - test_same("String.fromCharCode('x')"); - test_same("String.fromCharCode('0.5')"); + test("String.fromCharCode('x')", "'\\0'"); + test("String.fromCharCode('0.5')", "'\\0'"); } } diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index 5431c35f83656e..36875636bc1213 100644 --- a/tasks/minsize/minsize.snap +++ b/tasks/minsize/minsize.snap @@ -15,7 +15,7 @@ Original | minified | minified | gzip | gzip | Fixture 1.01 MB | 460.34 kB | 458.89 kB | 126.86 kB | 126.71 kB | bundle.min.js -1.25 MB | 652.70 kB | 646.76 kB | 163.53 kB | 163.73 kB | three.js +1.25 MB | 652.70 kB | 646.76 kB | 163.54 kB | 163.73 kB | three.js 2.14 MB | 726.21 kB | 724.14 kB | 180.20 kB | 181.07 kB | victory.js