feat(minifier): improve constant evaluation (#8252)

This commit is contained in:
Boshen 2025-01-05 12:41:57 +00:00
parent e84f267a39
commit 4d8a08d2ac
6 changed files with 149 additions and 157 deletions

View file

@ -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,
}
}

View file

@ -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,
}
}
}

View file

@ -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<Expression<'a>> {
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<bool> {
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()");
fn test_fold_unary() {
test_same("!foo()");
test_same("~foo()");
test_same("-foo()");
// These cases are handled here.
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 <https://github.com/tc39/test262/blob/05c45a4c430ab6fee3e0c7f0d47d8a30d8876a6d/test/language/expressions/unsigned-right-shift/S9.6_A2.2.js>.
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()");
@ -1618,15 +1617,21 @@ mod test {
}
#[test]
fn test_fold_shift_right_zero_fill() {
test("10 >>> 1", "5");
test_same("-1 >>> 0");
fn test_fold_shift_left() {
test("1 << 3", "8");
test("1.2345 << 0", "1");
test_same("1 << 24");
}
#[test]
fn test_fold_shift_left() {
test("1 << 3", "8");
test_same("1 << 24");
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]

View file

@ -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>,

View file

@ -176,10 +176,11 @@ impl<'a> PeepholeReplaceKnownMethods {
string_lit: &StringLiteral<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Option<Expression<'a>> {
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<Expression<'a>> {
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'");
}
}

View file

@ -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