Skip to content

Commit 6c627df

Browse files
committed
feat(minifier): implement unary Math functions in known methods (#8781)
The rounding logic between Rust and JavaScript differs, necessitating manual handling. Furthermore, there is a potential risk of being eliminated from the test suite. Consequently, we must implement an approach that assigns to `x` to mitigate this risk.
1 parent ae7f670 commit 6c627df

File tree

1 file changed

+136
-75
lines changed

1 file changed

+136
-75
lines changed

crates/oxc_minifier/src/peephole/replace_known_methods.rs

Lines changed: 136 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ impl<'a> PeepholeOptimizations {
6565
"toString" => Self::try_fold_to_string(*span, arguments, object, ctx),
6666
"pow" => self.try_fold_pow(*span, arguments, object, ctx),
6767
"sqrt" | "cbrt" => Self::try_fold_roots(*span, arguments, name, object, ctx),
68+
"abs" | "ceil" | "floor" | "round" | "fround" | "trunc" | "sign" => {
69+
Self::try_fold_math_unary(*span, arguments, name, object, ctx)
70+
}
6871
_ => None,
6972
};
7073
if let Some(replacement) = replacement {
@@ -348,6 +351,15 @@ impl<'a> PeepholeOptimizations {
348351
result.into_iter().rev().collect()
349352
}
350353

354+
fn validate_global_reference(expr: &Expression<'a>, target: &str, ctx: Ctx<'a, '_>) -> bool {
355+
let Expression::Identifier(ident) = expr else { return false };
356+
ctx.is_global_reference(ident) && ident.name == target
357+
}
358+
359+
fn validate_arguments(args: &Arguments, expected_len: usize) -> bool {
360+
args.len() == expected_len && args.iter().all(Argument::is_expression)
361+
}
362+
351363
/// `Math.pow(a, b)` -> `+(a) ** +b`
352364
fn try_fold_pow(
353365
&self,
@@ -359,12 +371,9 @@ impl<'a> PeepholeOptimizations {
359371
if self.target < ESTarget::ES2016 {
360372
return None;
361373
}
362-
363-
let Expression::Identifier(ident) = object else { return None };
364-
if ident.name != "Math" || !ctx.is_global_reference(ident) {
365-
return None;
366-
}
367-
if arguments.len() != 2 || arguments.iter().any(|arg| !arg.is_expression()) {
374+
if !Self::validate_global_reference(object, "Math", ctx)
375+
|| !Self::validate_arguments(arguments, 2)
376+
{
368377
return None;
369378
}
370379

@@ -404,11 +413,9 @@ impl<'a> PeepholeOptimizations {
404413
object: &Expression<'a>,
405414
ctx: Ctx<'a, '_>,
406415
) -> Option<Expression<'a>> {
407-
let Expression::Identifier(ident) = object else { return None };
408-
if ident.name != "Math" || !ctx.is_global_reference(ident) {
409-
return None;
410-
}
411-
if arguments.len() != 1 || !arguments[0].is_expression() {
416+
if !Self::validate_global_reference(object, "Math", ctx)
417+
|| !Self::validate_arguments(arguments, 1)
418+
{
412419
return None;
413420
}
414421
let arg_val = ctx.get_side_free_number_value(arguments[0].to_expression())?;
@@ -433,15 +440,54 @@ impl<'a> PeepholeOptimizations {
433440
"cbrt" => arg_val.cbrt(),
434441
_ => unreachable!(),
435442
};
436-
if calculated_val.fract() == 0.0 {
437-
return Some(ctx.ast.expression_numeric_literal(
438-
span,
439-
calculated_val,
440-
None,
441-
NumberBase::Decimal,
442-
));
443+
(calculated_val.fract() == 0.0).then(|| {
444+
ctx.ast.expression_numeric_literal(span, calculated_val, None, NumberBase::Decimal)
445+
})
446+
}
447+
448+
fn try_fold_math_unary(
449+
span: Span,
450+
arguments: &Arguments,
451+
name: &str,
452+
object: &Expression<'a>,
453+
ctx: Ctx<'a, '_>,
454+
) -> Option<Expression<'a>> {
455+
if !Self::validate_global_reference(object, "Math", ctx)
456+
|| !Self::validate_arguments(arguments, 1)
457+
{
458+
return None;
443459
}
444-
None
460+
let arg_val = ctx.get_side_free_number_value(arguments[0].to_expression())?;
461+
let result = match name {
462+
"abs" => arg_val.abs(),
463+
"ceil" => arg_val.ceil(),
464+
"floor" => arg_val.floor(),
465+
"round" => {
466+
// We should be aware that the behavior in JavaScript and Rust towards `round` is different.
467+
// In Rust, when facing `.5`, it may follow `half-away-from-zero` instead of round to upper bound.
468+
// So we need to handle it manually.
469+
let frac_part = arg_val.fract();
470+
let epsilon = 2f64.powf(-52f64);
471+
if (frac_part.abs() - 0.5).abs() < epsilon {
472+
// We should ceil it.
473+
arg_val.ceil()
474+
} else {
475+
arg_val.round()
476+
}
477+
}
478+
#[allow(clippy::cast_possible_truncation)]
479+
"fround" if arg_val.fract() == 0f64 || arg_val.is_nan() || arg_val.is_infinite() => {
480+
f64::from(arg_val as f32)
481+
}
482+
"fround" => return None,
483+
"trunc" => arg_val.trunc(),
484+
"sign" if arg_val.to_bits() == 0f64.to_bits() => 0f64,
485+
"sign" if arg_val.to_bits() == (-0f64).to_bits() => -0f64,
486+
"sign" => arg_val.signum(),
487+
_ => unreachable!(),
488+
};
489+
// These results are always shorter to return as a number, so we can just return them as NumericLiteral.
490+
Some(ctx.ast.expression_numeric_literal(span, result, None, NumberBase::Decimal))
445491
}
446492

447493
/// `[].concat(a).concat(b)` -> `[].concat(a, b)`
@@ -705,6 +751,14 @@ mod test {
705751
assert_eq!(run(code, Some(opts)), run(expected, None));
706752
}
707753

754+
fn test_value(code: &str, expected: &str) {
755+
test(format!("x = {code}").as_str(), format!("x = {expected}").as_str());
756+
}
757+
758+
fn test_same_value(code: &str) {
759+
test_same(format!("x = {code}").as_str());
760+
}
761+
708762
#[test]
709763
fn test_string_index_of() {
710764
test("x = 'abcdef'.indexOf('g')", "x = -1");
@@ -1108,99 +1162,106 @@ mod test {
11081162
}
11091163

11101164
#[test]
1111-
#[ignore]
11121165
fn test_fold_math_functions_abs() {
1113-
test_same("Math.abs(Math.random())");
1114-
1115-
test("Math.abs('-1')", "1");
1116-
test("Math.abs(-2)", "2");
1117-
test("Math.abs(null)", "0");
1118-
test("Math.abs('')", "0");
1119-
test("Math.abs([])", "0");
1120-
test("Math.abs([2])", "2");
1121-
test("Math.abs([1,2])", "NaN");
1122-
test("Math.abs({})", "NaN");
1123-
test("Math.abs('string');", "NaN");
1166+
test_same_value("Math.abs(Math.random())");
1167+
1168+
test_value("Math.abs('-1')", "1");
1169+
test_value("Math.abs(-2)", "2");
1170+
test_value("Math.abs(null)", "0");
1171+
test_value("Math.abs('')", "0");
1172+
test_value("Math.abs(NaN)", "NaN");
1173+
test_value("Math.abs(-0)", "0");
1174+
test_value("Math.abs(-Infinity)", "Infinity");
1175+
// TODO
1176+
// test_value("Math.abs([])", "0");
1177+
// test_value("Math.abs([2])", "2");
1178+
// test_value("Math.abs([1,2])", "NaN");
1179+
test_value("Math.abs({})", "NaN");
1180+
test_value("Math.abs('string');", "NaN");
11241181
}
11251182

11261183
#[test]
11271184
#[ignore]
11281185
fn test_fold_math_functions_imul() {
1129-
test_same("Math.imul(Math.random(),2)");
1130-
test("Math.imul(-1,1)", "-1");
1131-
test("Math.imul(2,2)", "4");
1132-
test("Math.imul(2)", "0");
1133-
test("Math.imul(2,3,5)", "6");
1134-
test("Math.imul(0xfffffffe, 5)", "-10");
1135-
test("Math.imul(0xffffffff, 5)", "-5");
1136-
test("Math.imul(0xfffffffffffff34f, 0xfffffffffff342)", "13369344");
1137-
test("Math.imul(0xfffffffffffff34f, -0xfffffffffff342)", "-13369344");
1138-
test("Math.imul(NaN, 2)", "0");
1186+
test_same_value("Math.imul(Math.random(),2)");
1187+
test_value("Math.imul(-1,1)", "-1");
1188+
test_value("Math.imul(2,2)", "4");
1189+
test_value("Math.imul(2)", "0");
1190+
test_value("Math.imul(2,3,5)", "6");
1191+
test_value("Math.imul(0xfffffffe, 5)", "-10");
1192+
test_value("Math.imul(0xffffffff, 5)", "-5");
1193+
test_value("Math.imul(0xfffffffffffff34f, 0xfffffffffff342)", "13369344");
1194+
test_value("Math.imul(0xfffffffffffff34f, -0xfffffffffff342)", "-13369344");
1195+
test_value("Math.imul(NaN, 2)", "0");
11391196
}
11401197

11411198
#[test]
1142-
#[ignore]
11431199
fn test_fold_math_functions_ceil() {
1144-
test_same("Math.ceil(Math.random())");
1200+
test_same_value("Math.ceil(Math.random())");
11451201

1146-
test("Math.ceil(1)", "1");
1147-
test("Math.ceil(1.5)", "2");
1148-
test("Math.ceil(1.3)", "2");
1149-
test("Math.ceil(-1.3)", "-1");
1202+
test_value("Math.ceil(1)", "1");
1203+
test_value("Math.ceil(1.5)", "2");
1204+
test_value("Math.ceil(1.3)", "2");
1205+
test_value("Math.ceil(-1.3)", "-1");
11501206
}
11511207

11521208
#[test]
1153-
#[ignore]
11541209
fn test_fold_math_functions_floor() {
1155-
test_same("Math.floor(Math.random())");
1210+
test_same_value("Math.floor(Math.random())");
11561211

1157-
test("Math.floor(1)", "1");
1158-
test("Math.floor(1.5)", "1");
1159-
test("Math.floor(1.3)", "1");
1160-
test("Math.floor(-1.3)", "-2");
1212+
test_value("Math.floor(1)", "1");
1213+
test_value("Math.floor(1.5)", "1");
1214+
test_value("Math.floor(1.3)", "1");
1215+
test_value("Math.floor(-1.3)", "-2");
11611216
}
11621217

11631218
#[test]
1164-
#[ignore]
11651219
fn test_fold_math_functions_fround() {
1166-
test_same("Math.fround(Math.random())");
1220+
test_same_value("Math.fround(Math.random())");
11671221

1168-
test("Math.fround(NaN)", "NaN");
1169-
test("Math.fround(Infinity)", "Infinity");
1170-
test("Math.fround(1)", "1");
1171-
test("Math.fround(0)", "0");
1222+
test_value("Math.fround(NaN)", "NaN");
1223+
test_value("Math.fround(Infinity)", "Infinity");
1224+
test_value("Math.fround(-Infinity)", "-Infinity");
1225+
test_value("Math.fround(1)", "1");
1226+
test_value("Math.fround(0)", "0");
1227+
test_value("Math.fround(16777217)", "16777216");
1228+
test_value("Math.fround(16777218)", "16777218");
11721229
}
11731230

11741231
#[test]
11751232
fn test_fold_math_functions_fround_j2cl() {
1176-
test_same("Math.fround(1.2)");
1233+
test_same_value("Math.fround(1.2)");
11771234
}
11781235

11791236
#[test]
1180-
#[ignore]
11811237
fn test_fold_math_functions_round() {
1182-
test_same("Math.round(Math.random())");
1183-
test("Math.round(NaN)", "NaN");
1184-
test("Math.round(3.5)", "4");
1185-
test("Math.round(-3.5)", "-3");
1238+
test_same_value("Math.round(Math.random())");
1239+
test_value("Math.round(NaN)", "NaN");
1240+
test_value("Math.round(3)", "3");
1241+
test_value("Math.round(3.5)", "4");
1242+
test_value("Math.round(-3.5)", "-3");
11861243
}
11871244

11881245
#[test]
1189-
#[ignore]
11901246
fn test_fold_math_functions_sign() {
1191-
test_same("Math.sign(Math.random())");
1192-
test("Math.sign(NaN)", "NaN");
1193-
test("Math.sign(3.5)", "1");
1194-
test("Math.sign(-3.5)", "-1");
1247+
test_same_value("Math.sign(Math.random())");
1248+
test_value("Math.sign(NaN)", "NaN");
1249+
test_value("Math.sign(0.0)", "0");
1250+
test_value("Math.sign(-0.0)", "-0");
1251+
test_value("Math.sign(0.01)", "1");
1252+
test_value("Math.sign(-0.01)", "-1");
1253+
test_value("Math.sign(3.5)", "1");
1254+
test_value("Math.sign(-3.5)", "-1");
11951255
}
11961256

11971257
#[test]
1198-
#[ignore]
11991258
fn test_fold_math_functions_trunc() {
1200-
test_same("Math.trunc(Math.random())");
1201-
test("Math.sign(NaN)", "NaN");
1202-
test("Math.trunc(3.5)", "3");
1203-
test("Math.trunc(-3.5)", "-3");
1259+
test_same_value("Math.trunc(Math.random())");
1260+
test_value("Math.sign(NaN)", "NaN");
1261+
test_value("Math.trunc(3.5)", "3");
1262+
test_value("Math.trunc(-3.5)", "-3");
1263+
test_value("Math.trunc(0.5)", "0");
1264+
test_value("Math.trunc(-0.5)", "-0");
12041265
}
12051266

12061267
#[test]

0 commit comments

Comments
 (0)