From f730c03dc7be43c3c908fbd6e106120a99ebb4a0 Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Mon, 11 Jul 2022 14:23:39 -0400 Subject: [PATCH] fix #2383: cross-platform hex number consistency --- CHANGELOG.md | 6 ++++++ internal/js_printer/js_printer.go | 11 +++++++++-- internal/js_printer/js_printer_test.go | 14 ++++++++++---- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a6d44ea2b2..f657167e9fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +* Fix a cross-platform consistency bug ([#2383](https://github.com/evanw/esbuild/issues/2383)) + + Previously esbuild would minify `0xFFFF_FFFF_FFFF_FFFF` as `0xffffffffffffffff` (18 bytes) on arm64 chips and as `18446744073709552e3` (19 bytes) on x86_64 chips. The reason was that the number was converted to a 64-bit unsigned integer internally for printing as hexadecimal, the 64-bit floating-point number `0xFFFF_FFFF_FFFF_FFFF` is actually `0x1_0000_0000_0000_0180` (i.e. it's rounded up, not down), and converting `float64` to `uint64` is implementation-dependent in Go when the input is out of bounds. This was fixed by changing the upper limit for which esbuild uses hexadecimal numbers during minification to `0xFFFF_FFFF_FFFF_F800`, which is the next representable 64-bit floating-point number below `0x1_0000_0000_0000_0180`, and which fits in a `uint64`. As a result, esbuild will now consistently never minify `0xFFFF_FFFF_FFFF_FFFF` as `0xffffffffffffffff` anymore, which means the output should now be consistent across platforms. + ## 0.14.49 * Keep inlined constants when direct `eval` is present ([#2361](https://github.com/evanw/esbuild/issues/2361)) diff --git a/internal/js_printer/js_printer.go b/internal/js_printer/js_printer.go index e0f77992385..848644f997d 100644 --- a/internal/js_printer/js_printer.go +++ b/internal/js_printer/js_printer.go @@ -2743,8 +2743,15 @@ func (p *printer) printNonNegativeFloat(absValue float64) { } } - // Numbers in this range can potentially be printed with one fewer byte as hex - if p.options.MinifyWhitespace && absValue >= 1_000_000_000_000 && absValue <= 0xFFFF_FFFF_FFFF_FFFF { + // Numbers in this range can potentially be printed with one fewer byte as + // hex. This compares against 0xFFFF_FFFF_FFFF_F800 instead of comparing + // against 0xFFFF_FFFF_FFFF_FFFF because 0xFFFF_FFFF_FFFF_FFFF when converted + // to float64 rounds up to 0x1_0000_0000_0000_0180, which can no longer fit + // into uint64. In Go, the result of converting float64 to uint64 outside of + // the uint64 range is implementation-dependent and is different on amd64 vs. + // arm64. The float64 value 0xFFFF_FFFF_FFFF_F800 is the biggest value that + // is below the float64 value 0x1_0000_0000_0000_0180, so we use that instead. + if p.options.MinifyWhitespace && absValue >= 1_000_000_000_000 && absValue <= 0xFFFF_FFFF_FFFF_F800 { if asInt := uint64(absValue); absValue == float64(asInt) { if hex := strconv.FormatUint(asInt, 16); 2+len(hex) < len(result) { result = append(append(result[:0], '0', 'x'), hex...) diff --git a/internal/js_printer/js_printer_test.go b/internal/js_printer/js_printer_test.go index 95c39bfd59d..e49c1cb4319 100644 --- a/internal/js_printer/js_printer_test.go +++ b/internal/js_printer/js_printer_test.go @@ -266,12 +266,18 @@ func TestNumber(t *testing.T) { // Check the hex vs. decimal decision boundary when minifying expectPrinted(t, "x = 999999999999", "x = 999999999999;\n") expectPrinted(t, "x = 1000000000001", "x = 1000000000001;\n") - expectPrinted(t, "x = 0xFFFFFFFFFFFFF80", "x = 1152921504606846800;\n") - expectPrinted(t, "x = 0x1000000000000000", "x = 1152921504606847e3;\n") + expectPrinted(t, "x = 0x0FFF_FFFF_FFFF_FF80", "x = 1152921504606846800;\n") + expectPrinted(t, "x = 0x1000_0000_0000_0000", "x = 1152921504606847e3;\n") + expectPrinted(t, "x = 0xFFFF_FFFF_FFFF_F000", "x = 18446744073709548e3;\n") + expectPrinted(t, "x = 0xFFFF_FFFF_FFFF_F800", "x = 1844674407370955e4;\n") + expectPrinted(t, "x = 0xFFFF_FFFF_FFFF_FFFF", "x = 18446744073709552e3;\n") expectPrintedMinify(t, "x = 999999999999", "x=999999999999;") expectPrintedMinify(t, "x = 1000000000001", "x=0xe8d4a51001;") - expectPrintedMinify(t, "x = 0xFFFFFFFFFFFFF80", "x=0xfffffffffffff80;") - expectPrintedMinify(t, "x = 0x1000000000000000", "x=1152921504606847e3;") + expectPrintedMinify(t, "x = 0x0FFF_FFFF_FFFF_FF80", "x=0xfffffffffffff80;") + expectPrintedMinify(t, "x = 0x1000_0000_0000_0000", "x=1152921504606847e3;") + expectPrintedMinify(t, "x = 0xFFFF_FFFF_FFFF_F000", "x=0xfffffffffffff000;") + expectPrintedMinify(t, "x = 0xFFFF_FFFF_FFFF_F800", "x=1844674407370955e4;") + expectPrintedMinify(t, "x = 0xFFFF_FFFF_FFFF_FFFF", "x=18446744073709552e3;") } func TestArray(t *testing.T) {