diff --git a/CHANGELOG.md b/CHANGELOG.md index 6381ec27c6..1b97f558a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,21 @@ } ``` +* Constant folding for JavaScript inequality operators ([#3645](https://github.com/evanw/esbuild/issues/3645)) + + This release introduces constant folding for the `< > <= >=` operators. The minifier will now replace these operators with `true` or `false` when both sides are compile-time numeric or string constants: + + ```js + // Original code + console.log(1 < 2, '🍕' > '🧀') + + // Old output (with --minify) + console.log(1<2,"🍕">"🧀"); + + // New output (with --minify) + console.log(!0,!1); + ``` + * Fix cross-platform non-determinism with CSS color space transformations ([#3650](https://github.com/evanw/esbuild/issues/3650)) The Go compiler takes advantage of "fused multiply and add" (FMA) instructions on certain processors which do the operation `x*y + z` without intermediate rounding. This causes esbuild's CSS color space math to differ on different processors (currently `ppc64le` and `s390x`), which breaks esbuild's guarantee of deterministic output. To avoid this, esbuild's color space math now inserts a `float64()` cast around every single math operation. This tells the Go compiler not to use the FMA optimization. diff --git a/internal/bundler_tests/snapshots/snapshots_dce.txt b/internal/bundler_tests/snapshots/snapshots_dce.txt index 18483cc552..8e363e6d17 100644 --- a/internal/bundler_tests/snapshots/snapshots_dce.txt +++ b/internal/bundler_tests/snapshots/snapshots_dce.txt @@ -274,10 +274,10 @@ console.log([ 3 /* a */ % 6 /* b */, 3 /* a */ ** 6 /* b */ ], [ - 3 /* a */ < 6 /* b */, - 3 /* a */ > 6 /* b */, - 3 /* a */ <= 6 /* b */, - 3 /* a */ >= 6 /* b */, + !0, + !1, + !0, + !1, 3 /* a */ == 6 /* b */, 3 /* a */ != 6 /* b */, 3 /* a */ === 6 /* b */, @@ -312,10 +312,10 @@ console.log([ 3 % 6, 3 ** 6 ], [ - 3 < 6, - 3 > 6, - 3 <= 6, - 3 >= 6, + !0, + !1, + !0, + !1, 3 == 6, 3 != 6, 3 === 6, diff --git a/internal/js_ast/js_ast_helpers.go b/internal/js_ast/js_ast_helpers.go index c51afd335c..705cdc69f3 100644 --- a/internal/js_ast/js_ast_helpers.go +++ b/internal/js_ast/js_ast_helpers.go @@ -1086,6 +1086,45 @@ func extractNumericValues(left Expr, right Expr) (float64, float64, bool) { return 0, 0, false } +func extractStringValue(data E) ([]uint16, bool) { + switch e := data.(type) { + case *EAnnotation: + return extractStringValue(e.Value.Data) + + case *EInlinedEnum: + return extractStringValue(e.Value.Data) + + case *EString: + return e.Value, true + } + + return nil, false +} + +func extractStringValues(left Expr, right Expr) ([]uint16, []uint16, bool) { + if a, ok := extractStringValue(left.Data); ok { + if b, ok := extractStringValue(right.Data); ok { + return a, b, true + } + } + return nil, nil, false +} + +func stringCompareUCS2(a []uint16, b []uint16) int { + var n int + if len(a) < len(b) { + n = len(a) + } else { + n = len(b) + } + for i := 0; i < n; i++ { + if delta := int(a[i]) - int(b[i]); delta != 0 { + return delta + } + } + return len(a) - len(b) +} + func approximatePrintedIntCharCount(intValue float64) int { count := 1 + (int)(math.Max(0, math.Floor(math.Log10(math.Abs(intValue))))) if intValue < 0 { @@ -1106,7 +1145,11 @@ func ShouldFoldBinaryArithmeticWhenMinifying(binary *EBinary) bool { // are unlikely to result in larger output. BinOpBitwiseAnd, BinOpBitwiseOr, - BinOpBitwiseXor: + BinOpBitwiseXor, + BinOpLt, + BinOpGt, + BinOpLe, + BinOpGe: return true case BinOpAdd: @@ -1221,6 +1264,38 @@ func FoldBinaryArithmetic(loc logger.Loc, e *EBinary) Expr { if left, right, ok := extractNumericValues(e.Left, e.Right); ok { return Expr{Loc: loc, Data: &ENumber{Value: float64(ToInt32(left) ^ ToInt32(right))}} } + + case BinOpLt: + if left, right, ok := extractNumericValues(e.Left, e.Right); ok { + return Expr{Loc: loc, Data: &EBoolean{Value: left < right}} + } + if left, right, ok := extractStringValues(e.Left, e.Right); ok { + return Expr{Loc: loc, Data: &EBoolean{Value: stringCompareUCS2(left, right) < 0}} + } + + case BinOpGt: + if left, right, ok := extractNumericValues(e.Left, e.Right); ok { + return Expr{Loc: loc, Data: &EBoolean{Value: left > right}} + } + if left, right, ok := extractStringValues(e.Left, e.Right); ok { + return Expr{Loc: loc, Data: &EBoolean{Value: stringCompareUCS2(left, right) > 0}} + } + + case BinOpLe: + if left, right, ok := extractNumericValues(e.Left, e.Right); ok { + return Expr{Loc: loc, Data: &EBoolean{Value: left <= right}} + } + if left, right, ok := extractStringValues(e.Left, e.Right); ok { + return Expr{Loc: loc, Data: &EBoolean{Value: stringCompareUCS2(left, right) <= 0}} + } + + case BinOpGe: + if left, right, ok := extractNumericValues(e.Left, e.Right); ok { + return Expr{Loc: loc, Data: &EBoolean{Value: left >= right}} + } + if left, right, ok := extractStringValues(e.Left, e.Right); ok { + return Expr{Loc: loc, Data: &EBoolean{Value: stringCompareUCS2(left, right) >= 0}} + } } return Expr{} diff --git a/internal/js_parser/js_parser_test.go b/internal/js_parser/js_parser_test.go index 88eefb54e3..57bc7568e1 100644 --- a/internal/js_parser/js_parser_test.go +++ b/internal/js_parser/js_parser_test.go @@ -4653,15 +4653,31 @@ func TestMangleBinaryConstantFolding(t *testing.T) { expectPrintedNormalAndMangle(t, "x = -123 / 0", "x = -123 / 0;\n", "x = -Infinity;\n") expectPrintedNormalAndMangle(t, "x = -123 / -0", "x = -123 / -0;\n", "x = Infinity;\n") - expectPrintedNormalAndMangle(t, "x = 3 < 6", "x = 3 < 6;\n", "x = 3 < 6;\n") - expectPrintedNormalAndMangle(t, "x = 3 > 6", "x = 3 > 6;\n", "x = 3 > 6;\n") - expectPrintedNormalAndMangle(t, "x = 3 <= 6", "x = 3 <= 6;\n", "x = 3 <= 6;\n") - expectPrintedNormalAndMangle(t, "x = 3 >= 6", "x = 3 >= 6;\n", "x = 3 >= 6;\n") + expectPrintedNormalAndMangle(t, "x = 3 < 6", "x = 3 < 6;\n", "x = true;\n") + expectPrintedNormalAndMangle(t, "x = 3 > 6", "x = 3 > 6;\n", "x = false;\n") + expectPrintedNormalAndMangle(t, "x = 3 <= 6", "x = 3 <= 6;\n", "x = true;\n") + expectPrintedNormalAndMangle(t, "x = 3 >= 6", "x = 3 >= 6;\n", "x = false;\n") expectPrintedNormalAndMangle(t, "x = 3 == 6", "x = false;\n", "x = false;\n") expectPrintedNormalAndMangle(t, "x = 3 != 6", "x = true;\n", "x = true;\n") expectPrintedNormalAndMangle(t, "x = 3 === 6", "x = false;\n", "x = false;\n") expectPrintedNormalAndMangle(t, "x = 3 !== 6", "x = true;\n", "x = true;\n") + expectPrintedNormalAndMangle(t, "x = 'a' < 'b'", "x = \"a\" < \"b\";\n", "x = true;\n") + expectPrintedNormalAndMangle(t, "x = 'a' > 'b'", "x = \"a\" > \"b\";\n", "x = false;\n") + expectPrintedNormalAndMangle(t, "x = 'a' <= 'b'", "x = \"a\" <= \"b\";\n", "x = true;\n") + expectPrintedNormalAndMangle(t, "x = 'a' >= 'b'", "x = \"a\" >= \"b\";\n", "x = false;\n") + + expectPrintedNormalAndMangle(t, "x = 'ab' < 'abc'", "x = \"ab\" < \"abc\";\n", "x = true;\n") + expectPrintedNormalAndMangle(t, "x = 'ab' > 'abc'", "x = \"ab\" > \"abc\";\n", "x = false;\n") + expectPrintedNormalAndMangle(t, "x = 'ab' <= 'abc'", "x = \"ab\" <= \"abc\";\n", "x = true;\n") + expectPrintedNormalAndMangle(t, "x = 'ab' >= 'abc'", "x = \"ab\" >= \"abc\";\n", "x = false;\n") + + // This checks for comparing by code point vs. by code unit + expectPrintedNormalAndMangle(t, "x = '𐙩' < 'ﬡ'", "x = \"𐙩\" < \"ﬡ\";\n", "x = true;\n") + expectPrintedNormalAndMangle(t, "x = '𐙩' > 'ﬡ'", "x = \"𐙩\" > \"ﬡ\";\n", "x = false;\n") + expectPrintedNormalAndMangle(t, "x = '𐙩' <= 'ﬡ'", "x = \"𐙩\" <= \"ﬡ\";\n", "x = true;\n") + expectPrintedNormalAndMangle(t, "x = '𐙩' >= 'ﬡ'", "x = \"𐙩\" >= \"ﬡ\";\n", "x = false;\n") + expectPrintedNormalAndMangle(t, "x = 3 in 6", "x = 3 in 6;\n", "x = 3 in 6;\n") expectPrintedNormalAndMangle(t, "x = 3 instanceof 6", "x = 3 instanceof 6;\n", "x = 3 instanceof 6;\n") expectPrintedNormalAndMangle(t, "x = (3, 6)", "x = (3, 6);\n", "x = 6;\n")