Skip to content

Commit

Permalink
feat(codegen): implement minify number from terser (#5929)
Browse files Browse the repository at this point in the history
  • Loading branch information
Boshen committed Sep 20, 2024
1 parent 6757c58 commit d901772
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 126 deletions.
112 changes: 43 additions & 69 deletions crates/oxc_codegen/src/gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1110,16 +1110,17 @@ impl<'a> Gen for NumericLiteral<'a> {
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
fn gen(&self, p: &mut Codegen, _ctx: Context) {
p.add_source_mapping(self.span.start);
if self.value != f64::INFINITY && (p.options.minify || self.raw.is_empty()) {
if !p.options.minify && !self.raw.is_empty() {
p.print_str(self.raw);
need_space_before_dot(self.raw, p);
} else if self.value != f64::INFINITY {
p.print_space_before_identifier();
let abs_value = self.value.abs();

if self.value.is_sign_negative() {
p.print_space_before_operator(Operator::Unary(UnaryOperator::UnaryNegation));
p.print_str("-");
}

let result = print_non_negative_float(abs_value, p);
let result = get_minified_number(abs_value);
let bytes = result.as_str();
p.print_str(bytes);
need_space_before_dot(bytes, p);
Expand All @@ -1133,78 +1134,51 @@ impl<'a> Gen for NumericLiteral<'a> {
}
}

// TODO: refactor this with less allocations
// <https://github.com/evanw/esbuild/blob/360d47230813e67d0312ad754cad2b6ee09b151b/internal/js_printer/js_printer.go#L3472>
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn print_non_negative_float(value: f64, p: &Codegen) -> String {
// https://github.com/terser/terser/blob/c5315c3fd6321d6b2e076af35a70ef532f498505/lib/output.js#L2418
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_possible_wrap)]
fn get_minified_number(num: f64) -> String {
use oxc_syntax::number::ToJsString;
if value < 1000.0 && value.fract() == 0.0 {
return value.to_js_string();
}
let mut result = format!("{value:e}");
let chars = result.as_bytes();
let len = chars.len();
let dot = chars.iter().position(|&c| c == b'.');
let u8_to_string = |num: &[u8]| {
// SAFETY: criteria of `from_utf8_unchecked`.are met.
unsafe { String::from_utf8_unchecked(num.to_vec()) }
};

if dot == Some(1) && chars[0] == b'0' {
// Strip off the leading zero when minifying
// "0.5" => ".5"
let stripped_result = &chars[1..];
// after stripping the leading zero, the after dot position will be start from 1
let after_dot = 1;

// Try using an exponent
// "0.001" => "1e-3"
if stripped_result[after_dot] == b'0' {
let mut i = after_dot + 1;
while stripped_result[i] == b'0' {
i += 1;
}
let remaining = &stripped_result[i..];
let exponent = format!("-{}", remaining.len() - after_dot + i);

// Only switch if it's actually shorter
if stripped_result.len() > remaining.len() + 1 + exponent.len() {
result = format!("{}e{}", u8_to_string(remaining), exponent);
} else {
result = u8_to_string(stripped_result);
}
} else {
result = u8_to_string(stripped_result);
}
} else if chars[len - 1] == b'0' {
// Simplify numbers ending with "0" by trying to use an exponent
// "1000" => "1e3"
let mut i = len - 1;
while i > 0 && chars[i - 1] == b'0' {
i -= 1;
}
let remaining = &chars[0..i];
let exponent = format!("{}", chars.len() - i);
if num < 1000.0 && num.fract() == 0.0 {
return num.to_js_string();
}

// Only switch if it's actually shorter
if chars.len() > remaining.len() + 1 + exponent.len() {
result = format!("{}e{}", u8_to_string(remaining), exponent);
} else {
result = u8_to_string(chars);
}
let mut s = num.to_js_string();

if s.starts_with("0.") {
s = s[1..].to_string();
}

if p.options.minify && value.fract() == 0.0 {
let value = value as u64;
if (1_000_000_000_000..=0xFFFF_FFFF_FFFF_F800).contains(&value) {
let hex = format!("{value:#x}");
if hex.len() < result.len() {
result = hex;
}
s = s.cow_replacen("e+", "e", 1).to_string();

let mut candidates = vec![s.clone()];

if num.fract() == 0.0 {
candidates.push(format!("0x{:x}", num as u128));
}

if s.starts_with(".0") {
// create `1e-2`
if let Some((i, _)) = s[1..].bytes().enumerate().find(|(_, c)| *c != b'0') {
let len = i + 1; // `+1` to include the dot.
let digits = &s[len..];
candidates.push(format!("{digits}e-{}", digits.len() + len - 1));
}
} else if s.ends_with('0') {
// create 1e2
if let Some((len, _)) = s.bytes().rev().enumerate().find(|(_, c)| *c != b'0') {
candidates.push(format!("{}e{len}", &s[0..s.len() - len]));
}
} else if let Some((integer, point, exponent)) =
s.split_once('.').and_then(|(a, b)| b.split_once('e').map(|e| (a, e.0, e.1)))
{
// `1.2e101` -> ("1", "2", "101")
candidates.push(format!(
"{integer}{point}e{}",
exponent.parse::<isize>().unwrap() - point.len() as isize
));
}

result
candidates.into_iter().min_by_key(String::len).unwrap()
}

impl<'a> Gen for BigIntLiteral<'a> {
Expand Down
92 changes: 46 additions & 46 deletions crates/oxc_codegen/tests/integration/esbuild.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@
use crate::tester::{test, test_minify};

// NOTE: These values are aligned with terser, not esbuild.
#[test]
#[ignore]
fn test_number() {
// Check "1eN"
test("x = 1e-100", "x = 1e-100;\n");
test("x = 1e-4", "x = 1e-4;\n");
test("x = 1e-3", "x = 1e-3;\n");
// test("x = 1e-2", "x = 0.01;\n");
// test("x = 1e-1", "x = 0.1;\n");
// test("x = 1e0", "x = 1;\n");
// test("x = 1e1", "x = 10;\n");
// test("x = 1e2", "x = 100;\n");
test("x = 1e-2", "x = 1e-2;\n");
test("x = 1e-1", "x = 1e-1;\n");
test("x = 1e0", "x = 1e0;\n");
test("x = 1e1", "x = 1e1;\n");
test("x = 1e2", "x = 1e2;\n");
test("x = 1e3", "x = 1e3;\n");
test("x = 1e4", "x = 1e4;\n");
test("x = 1e100", "x = 1e100;\n");
Expand All @@ -28,22 +28,22 @@ fn test_number() {
test_minify("x = 1e2", "x=100;");
test_minify("x = 1e3", "x=1e3;");
test_minify("x = 1e4", "x=1e4;");
// test_minify("x = 1e100", "x=1e100;");
test_minify("x = 1e100", "x=1e100;");

// Check "12eN"
test("x = 12e-100", "x = 12e-100;\n");
test("x = 12e-5", "x = 12e-5;\n");
test("x = 12e-4", "x = 12e-4;\n");
// test("x = 12e-3", "x = 0.012;\n");
// test("x = 12e-2", "x = 0.12;\n");
// test("x = 12e-1", "x = 1.2;\n");
// test("x = 12e0", "x = 12;\n");
// test("x = 12e1", "x = 120;\n");
// test("x = 12e2", "x = 1200;\n");
test("x = 12e-3", "x = 12e-3;\n");
test("x = 12e-2", "x = 12e-2;\n");
test("x = 12e-1", "x = 12e-1;\n");
test("x = 12e0", "x = 12e0;\n");
test("x = 12e1", "x = 12e1;\n");
test("x = 12e2", "x = 12e2;\n");
test("x = 12e3", "x = 12e3;\n");
test("x = 12e4", "x = 12e4;\n");
test("x = 12e100", "x = 12e100;\n");
// test_minify("x = 12e-100", "x=12e-100;");
test_minify("x = 12e-100", "x=1.2e-99;");
test_minify("x = 12e-6", "x=12e-6;");
test_minify("x = 12e-5", "x=12e-5;");
test_minify("x = 12e-4", "x=.0012;");
Expand All @@ -55,7 +55,7 @@ fn test_number() {
test_minify("x = 12e2", "x=1200;");
test_minify("x = 12e3", "x=12e3;");
test_minify("x = 12e4", "x=12e4;");
// test_minify("x = 12e100", "x=12e100;");
test_minify("x = 12e100", "x=12e100;");

// Check cases for "A.BeX" => "ABeY" simplification
test("x = 123456789", "x = 123456789;\n");
Expand All @@ -66,11 +66,11 @@ fn test_number() {
test("x = 10000123456789", "x = 10000123456789;\n");
test("x = 100000123456789", "x = 100000123456789;\n");
test("x = 1000000123456789", "x = 1000000123456789;\n");
// test("x = 10000000123456789", "x = 10000000123456788;\n");
// test("x = 100000000123456789", "x = 100000000123456780;\n");
// test("x = 1000000000123456789", "x = 1000000000123456800;\n");
// test("x = 10000000000123456789", "x = 10000000000123458e3;\n");
// test("x = 100000000000123456789", "x = 10000000000012345e4;\n");
test("x = 10000000123456789", "x = 10000000123456789;\n");
test("x = 100000000123456789", "x = 100000000123456789;\n");
test("x = 1000000000123456789", "x = 1000000000123456789;\n");
test("x = 10000000000123456789", "x = 10000000000123456789;\n");
test("x = 100000000000123456789", "x = 100000000000123456789;\n");

// Check numbers around the ends of various integer ranges. These were
// crashing in the WebAssembly build due to a bug in the Go runtime.
Expand All @@ -92,36 +92,36 @@ fn test_number() {
test_minify("x = -0x1_0000_0001", "x=-4294967297;");

// int64
test_minify("x = 0x7fff_ffff_ffff_fdff", "x=9223372036854775e3;");
test_minify("x = 0x8000_0000_0000_0000", "x=9223372036854776e3;");
test_minify("x = 0x8000_0000_0000_3000", "x=9223372036854788e3;");
test_minify("x = -0x7fff_ffff_ffff_fdff", "x=-9223372036854775e3;");
test_minify("x = -0x8000_0000_0000_0000", "x=-9223372036854776e3;");
test_minify("x = -0x8000_0000_0000_3000", "x=-9223372036854788e3;");
test_minify("x = 0x7fff_ffff_ffff_fdff", "x=0x7ffffffffffffc00;");
test_minify("x = 0x8000_0000_0000_0000", "x=0x8000000000000000;");
test_minify("x = 0x8000_0000_0000_3000", "x=0x8000000000003000;");
test_minify("x = -0x7fff_ffff_ffff_fdff", "x=-0x7ffffffffffffc00;");
test_minify("x = -0x8000_0000_0000_0000", "x=-0x8000000000000000;");
test_minify("x = -0x8000_0000_0000_3000", "x=-0x8000000000003000;");

// uint64
test_minify("x = 0xffff_ffff_ffff_fbff", "x=1844674407370955e4;");
test_minify("x = 0x1_0000_0000_0000_0000", "x=18446744073709552e3;");
test_minify("x = 0x1_0000_0000_0000_1000", "x=18446744073709556e3;");
test_minify("x = -0xffff_ffff_ffff_fbff", "x=-1844674407370955e4;");
test_minify("x = -0x1_0000_0000_0000_0000", "x=-18446744073709552e3;");
test_minify("x = -0x1_0000_0000_0000_1000", "x=-18446744073709556e3;");
test_minify("x = 0xffff_ffff_ffff_fbff", "x=0xfffffffffffff800;");
test_minify("x = 0x1_0000_0000_0000_0000", "x=0x10000000000000000;");
test_minify("x = 0x1_0000_0000_0000_1000", "x=0x10000000000001000;");
test_minify("x = -0xffff_ffff_ffff_fbff", "x=-0xfffffffffffff800;");
test_minify("x = -0x1_0000_0000_0000_0000", "x=-0x10000000000000000;");
test_minify("x = -0x1_0000_0000_0000_1000", "x=-0x10000000000001000;");

// Check the hex vs. decimal decision boundary when minifying
// test("x = 999999999999", "x = 999999999999;\n");
// test("x = 1000000000001", "x = 1000000000001;\n");
// test("x = 0x0FFF_FFFF_FFFF_FF80", "x = 1152921504606846800;\n");
// test("x = 0x1000_0000_0000_0000", "x = 1152921504606847e3;\n");
// test("x = 0xFFFF_FFFF_FFFF_F000", "x = 18446744073709548e3;\n");
// test("x = 0xFFFF_FFFF_FFFF_F800", "x = 1844674407370955e4;\n");
// test("x = 0xFFFF_FFFF_FFFF_FFFF", "x = 18446744073709552e3;\n");
// test_minify("x = 999999999999", "x=999999999999;");
// test_minify("x = 1000000000001", "x=0xe8d4a51001;");
// test_minify("x = 0x0FFF_FFFF_FFFF_FF80", "x=0xfffffffffffff80;");
// test_minify("x = 0x1000_0000_0000_0000", "x=1152921504606847e3;");
// test_minify("x = 0xFFFF_FFFF_FFFF_F000", "x=0xfffffffffffff000;");
// test_minify("x = 0xFFFF_FFFF_FFFF_F800", "x=1844674407370955e4;");
// test_minify("x = 0xFFFF_FFFF_FFFF_FFFF", "x=18446744073709552e3;");
test("x = 999999999999", "x = 999999999999;\n");
test("x = 1000000000001", "x = 1000000000001;\n");
test("x = 0x0FFF_FFFF_FFFF_FF80", "x = 0x0FFF_FFFF_FFFF_FF80;\n");
test("x = 0x1000_0000_0000_0000", "x = 0x1000_0000_0000_0000;\n");
test("x = 0xFFFF_FFFF_FFFF_F000", "x = 0xFFFF_FFFF_FFFF_F000;\n");
test("x = 0xFFFF_FFFF_FFFF_F800", "x = 0xFFFF_FFFF_FFFF_F800;\n");
test("x = 0xFFFF_FFFF_FFFF_FFFF", "x = 0xFFFF_FFFF_FFFF_FFFF;\n");
test_minify("x = 999999999999", "x=999999999999;");
test_minify("x = 1000000000001", "x=0xe8d4a51001;");
test_minify("x = 0x0FFF_FFFF_FFFF_FF80", "x=0xfffffffffffff80;");
test_minify("x = 0x1000_0000_0000_0000", "x=0x1000000000000000;");
test_minify("x = 0xFFFF_FFFF_FFFF_F000", "x=0xfffffffffffff000;");
test_minify("x = 0xFFFF_FFFF_FFFF_F800", "x=0xfffffffffffff800;");
test_minify("x = 0xFFFF_FFFF_FFFF_FFFF", "x=0x10000000000000000;");

// Check printing a space in between a number and a subsequent "."
test_minify("x = 0.0001 .y", "x=1e-4.y;");
Expand Down
22 changes: 11 additions & 11 deletions tasks/minsize/minsize.snap
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
Original | Minified | esbuild | Gzip | esbuild

72.14 kB | 24.37 kB | 23.70 kB | 8.73 kB | 8.54 kB | react.development.js
72.14 kB | 24.32 kB | 23.70 kB | 8.71 kB | 8.54 kB | react.development.js

173.90 kB | 61.83 kB | 59.82 kB | 19.59 kB | 19.33 kB | moment.js
173.90 kB | 61.79 kB | 59.82 kB | 19.57 kB | 19.33 kB | moment.js

287.63 kB | 92.93 kB | 90.07 kB | 32.35 kB | 31.95 kB | jquery.js
287.63 kB | 92.91 kB | 90.07 kB | 32.33 kB | 31.95 kB | jquery.js

342.15 kB | 122.97 kB | 118.14 kB | 45.05 kB | 44.37 kB | vue.js

544.10 kB | 73.57 kB | 72.48 kB | 26.14 kB | 26.20 kB | lodash.js
544.10 kB | 73.57 kB | 72.48 kB | 26.15 kB | 26.20 kB | lodash.js

555.77 kB | 275.81 kB | 270.13 kB | 91.66 kB | 90.80 kB | d3.js
555.77 kB | 274.81 kB | 270.13 kB | 91.40 kB | 90.80 kB | d3.js

1.01 MB | 471.78 kB | 458.89 kB | 127.61 kB | 126.71 kB | bundle.min.js
1.01 MB | 471.70 kB | 458.89 kB | 127.56 kB | 126.71 kB | bundle.min.js

1.25 MB | 676.14 kB | 646.76 kB | 167.18 kB | 163.73 kB | three.js
1.25 MB | 673.75 kB | 646.76 kB | 166.76 kB | 163.73 kB | three.js

2.14 MB | 751.46 kB | 724.14 kB | 182.74 kB | 181.07 kB | victory.js
2.14 MB | 743.40 kB | 724.14 kB | 181.95 kB | 181.07 kB | victory.js

3.20 MB | 1.03 MB | 1.01 MB | 332.60 kB | 331.56 kB | echarts.js
3.20 MB | 1.03 MB | 1.01 MB | 332.45 kB | 331.56 kB | echarts.js

6.69 MB | 2.42 MB | 2.31 MB | 503.22 kB | 488.28 kB | antd.js
6.69 MB | 2.42 MB | 2.31 MB | 503.16 kB | 488.28 kB | antd.js

10.95 MB | 3.60 MB | 3.49 MB | 915.21 kB | 915.50 kB | typescript.js
10.95 MB | 3.57 MB | 3.49 MB | 912.37 kB | 915.50 kB | typescript.js

0 comments on commit d901772

Please sign in to comment.