Skip to content

Commit

Permalink
fix #2394, fix #2895: more constant folding cases
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Feb 6, 2023
1 parent 22354e4 commit bfe3ead
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 24 deletions.
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,46 @@
# Changelog

## Unreleased

* Add constant folding for certain additional equality cases ([#2394](https://github.com/evanw/esbuild/issues/2394), [#2895](https://github.com/evanw/esbuild/issues/2895))

This release adds constant folding for expressions similar to the following:

```js
// Original input
console.log(
null === 'foo',
null === undefined,
null == undefined,
false === 0,
false == 0,
1 === true,
1 == true,
)

// Old output
console.log(
null === "foo",
null === void 0,
null == void 0,
false === 0,
false == 0,
1 === true,
1 == true
);

// New output
console.log(
false,
false,
true,
false,
true,
false,
true
);
```

## 0.17.6

* Fix a CSS parser crash on invalid CSS ([#2892](https://github.com/evanw/esbuild/issues/2892))
Expand Down
129 changes: 113 additions & 16 deletions internal/js_ast/js_ast_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -1082,41 +1082,138 @@ func IsBinaryNullAndUndefined(left Expr, right Expr, op OpCode) (Expr, Expr, boo
return Expr{}, Expr{}, false
}

type EqualityKind uint8

const (
LooseEquality EqualityKind = iota
StrictEquality
)

// Returns "equal, ok". If "ok" is false, then nothing is known about the two
// values. If "ok" is true, the equality or inequality of the two values is
// stored in "equal".
func CheckEqualityIfNoSideEffects(left E, right E) (bool, bool) {
func CheckEqualityIfNoSideEffects(left E, right E, kind EqualityKind) (equal bool, ok bool) {
if r, ok := right.(*EInlinedEnum); ok {
return CheckEqualityIfNoSideEffects(left, r.Value.Data)
return CheckEqualityIfNoSideEffects(left, r.Value.Data, kind)
}

switch l := left.(type) {
case *EInlinedEnum:
return CheckEqualityIfNoSideEffects(l.Value.Data, right)
return CheckEqualityIfNoSideEffects(l.Value.Data, right, kind)

case *ENull:
_, ok := right.(*ENull)
return ok, ok
switch right.(type) {
case *ENull:
// "null === null" is true
return true, true

case *EUndefined:
// "null == undefined" is true
// "null === undefined" is false
return kind == LooseEquality, true

default:
if IsPrimitiveLiteral(right) {
// "null == (not null or undefined)" is false
return false, true
}
}

case *EUndefined:
_, ok := right.(*EUndefined)
return ok, ok
switch right.(type) {
case *EUndefined:
// "undefined === undefined" is true
return true, true

case *ENull:
// "undefined == null" is true
// "undefined === null" is false
return kind == LooseEquality, true

default:
if IsPrimitiveLiteral(right) {
// "undefined == (not null or undefined)" is false
return false, true
}
}

case *EBoolean:
r, ok := right.(*EBoolean)
return ok && l.Value == r.Value, ok
switch r := right.(type) {
case *EBoolean:
// "false === false" is true
// "false === true" is false
return l.Value == r.Value, true

case *ENumber:
if kind == LooseEquality {
if l.Value {
// "true == 1" is true
return r.Value == 1, true
} else {
// "false == 0" is true
return r.Value == 0, true
}
} else {
// "true === 1" is false
// "false === 0" is false
return false, true
}

case *ENull, *EUndefined:
// "(not null or undefined) == undefined" is false
return false, true
}

case *ENumber:
r, ok := right.(*ENumber)
return ok && l.Value == r.Value, ok
switch r := right.(type) {
case *ENumber:
// "0 === 0" is true
// "0 === 1" is false
return l.Value == r.Value, true

case *EBoolean:
if kind == LooseEquality {
if r.Value {
// "1 == true" is true
return l.Value == 1, true
} else {
// "0 == false" is true
return l.Value == 0, true
}
} else {
// "1 === true" is false
// "0 === false" is false
return false, true
}

case *ENull, *EUndefined:
// "(not null or undefined) == undefined" is false
return false, true
}

case *EBigInt:
r, ok := right.(*EBigInt)
return ok && l.Value == r.Value, ok
switch r := right.(type) {
case *EBigInt:
// "0n === 0n" is true
// "0n === 1n" is false
return l.Value == r.Value, true

case *ENull, *EUndefined:
// "(not null or undefined) == undefined" is false
return false, true
}

case *EString:
r, ok := right.(*EString)
return ok && helpers.UTF16EqualsUTF16(l.Value, r.Value), ok
switch r := right.(type) {
case *EString:
// "'a' === 'a'" is true
// "'a' === 'b'" is false
return helpers.UTF16EqualsUTF16(l.Value, r.Value), true

case *ENull, *EUndefined:
// "(not null or undefined) == undefined" is false
return false, true
}
}

return false, false
Expand Down Expand Up @@ -1186,7 +1283,7 @@ func ValuesLookTheSame(left E, right E) bool {
}
}

equal, ok := CheckEqualityIfNoSideEffects(left, right)
equal, ok := CheckEqualityIfNoSideEffects(left, right, StrictEquality)
return ok && equal
}

Expand Down
8 changes: 4 additions & 4 deletions internal/js_parser/js_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -12646,7 +12646,7 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
}

case js_ast.BinOpLooseEq:
if result, ok := js_ast.CheckEqualityIfNoSideEffects(e.Left.Data, e.Right.Data); ok {
if result, ok := js_ast.CheckEqualityIfNoSideEffects(e.Left.Data, e.Right.Data, js_ast.LooseEquality); ok {
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EBoolean{Value: result}}, exprOut{}
}
afterOpLoc := locAfterOp(e)
Expand All @@ -12669,7 +12669,7 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
}

case js_ast.BinOpStrictEq:
if result, ok := js_ast.CheckEqualityIfNoSideEffects(e.Left.Data, e.Right.Data); ok {
if result, ok := js_ast.CheckEqualityIfNoSideEffects(e.Left.Data, e.Right.Data, js_ast.StrictEquality); ok {
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EBoolean{Value: result}}, exprOut{}
}
afterOpLoc := locAfterOp(e)
Expand All @@ -12690,7 +12690,7 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
}

case js_ast.BinOpLooseNe:
if result, ok := js_ast.CheckEqualityIfNoSideEffects(e.Left.Data, e.Right.Data); ok {
if result, ok := js_ast.CheckEqualityIfNoSideEffects(e.Left.Data, e.Right.Data, js_ast.LooseEquality); ok {
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EBoolean{Value: !result}}, exprOut{}
}
afterOpLoc := locAfterOp(e)
Expand All @@ -12713,7 +12713,7 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
}

case js_ast.BinOpStrictNe:
if result, ok := js_ast.CheckEqualityIfNoSideEffects(e.Left.Data, e.Right.Data); ok {
if result, ok := js_ast.CheckEqualityIfNoSideEffects(e.Left.Data, e.Right.Data, js_ast.StrictEquality); ok {
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EBoolean{Value: !result}}, exprOut{}
}
afterOpLoc := locAfterOp(e)
Expand Down
47 changes: 43 additions & 4 deletions internal/js_parser/js_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2542,10 +2542,15 @@ func TestConstantFolding(t *testing.T) {
expectPrinted(t, "x = null == null", "x = true;\n")
expectPrinted(t, "x = null != null", "x = false;\n")

expectPrinted(t, "x = undefined === null", "x = void 0 === null;\n")
expectPrinted(t, "x = undefined !== null", "x = void 0 !== null;\n")
expectPrinted(t, "x = undefined == null", "x = void 0 == null;\n")
expectPrinted(t, "x = undefined != null", "x = void 0 != null;\n")
expectPrinted(t, "x = null === undefined", "x = false;\n")
expectPrinted(t, "x = null !== undefined", "x = true;\n")
expectPrinted(t, "x = null == undefined", "x = true;\n")
expectPrinted(t, "x = null != undefined", "x = false;\n")

expectPrinted(t, "x = undefined === null", "x = false;\n")
expectPrinted(t, "x = undefined !== null", "x = true;\n")
expectPrinted(t, "x = undefined == null", "x = true;\n")
expectPrinted(t, "x = undefined != null", "x = false;\n")

expectPrinted(t, "x = true === true", "x = true;\n")
expectPrinted(t, "x = true === false", "x = false;\n")
Expand Down Expand Up @@ -2636,6 +2641,40 @@ func TestConstantFolding(t *testing.T) {
expectPrinted(t, "x = y + 4 + '5'", "x = y + 4 + \"5\";\n")
expectPrinted(t, "x = '3' + 4 + 5", "x = \"345\";\n")
expectPrinted(t, "x = 3 + 4 + '5'", "x = 3 + 4 + \"5\";\n")

expectPrinted(t, "x = null == 0", "x = false;\n")
expectPrinted(t, "x = 0 == null", "x = false;\n")
expectPrinted(t, "x = undefined == 0", "x = false;\n")
expectPrinted(t, "x = 0 == undefined", "x = false;\n")

expectPrinted(t, "x = null == NaN", "x = false;\n")
expectPrinted(t, "x = NaN == null", "x = false;\n")
expectPrinted(t, "x = undefined == NaN", "x = false;\n")
expectPrinted(t, "x = NaN == undefined", "x = false;\n")

expectPrinted(t, "x = null == ''", "x = false;\n")
expectPrinted(t, "x = '' == null", "x = false;\n")
expectPrinted(t, "x = undefined == ''", "x = false;\n")
expectPrinted(t, "x = '' == undefined", "x = false;\n")

expectPrinted(t, "x = null == 'null'", "x = false;\n")
expectPrinted(t, "x = 'null' == null", "x = false;\n")
expectPrinted(t, "x = undefined == 'undefined'", "x = false;\n")
expectPrinted(t, "x = 'undefined' == undefined", "x = false;\n")

expectPrinted(t, "x = false === 0", "x = false;\n")
expectPrinted(t, "x = true === 1", "x = false;\n")
expectPrinted(t, "x = false == 0", "x = true;\n")
expectPrinted(t, "x = false == -0", "x = true;\n")
expectPrinted(t, "x = true == 1", "x = true;\n")
expectPrinted(t, "x = true == 2", "x = false;\n")

expectPrinted(t, "x = 0 === false", "x = false;\n")
expectPrinted(t, "x = 1 === true", "x = false;\n")
expectPrinted(t, "x = 0 == false", "x = true;\n")
expectPrinted(t, "x = -0 == false", "x = true;\n")
expectPrinted(t, "x = 1 == true", "x = true;\n")
expectPrinted(t, "x = 2 == true", "x = false;\n")
}

func TestConstantFoldingScopes(t *testing.T) {
Expand Down

0 comments on commit bfe3ead

Please sign in to comment.