Skip to content

Commit

Permalink
fix #3645: constant folding for < > <= >=
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Feb 18, 2024
1 parent 5650831 commit 555db48
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 13 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 8 additions & 8 deletions internal/bundler_tests/snapshots/snapshots_dce.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 */,
Expand Down Expand Up @@ -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,
Expand Down
77 changes: 76 additions & 1 deletion internal/js_ast/js_ast_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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:
Expand Down Expand Up @@ -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{}
Expand Down
24 changes: 20 additions & 4 deletions internal/js_parser/js_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down

0 comments on commit 555db48

Please sign in to comment.