Skip to content

Commit

Permalink
JS: improve conditional and unary-not expression compression
Browse files Browse the repository at this point in the history
  • Loading branch information
tdewolff committed Apr 5, 2022
1 parent b6272e6 commit a2eb81a
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 103 deletions.
109 changes: 14 additions & 95 deletions js/js.go
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,12 @@ func (m *jsMinifier) minifyBinaryExpr(expr *js.BinaryExpr) bool {
}

func (m *jsMinifier) minifyExpr(i js.IExpr, prec js.OpPrec) {
if cond, ok := i.(*js.CondExpr); ok {
i = m.optimizeCondExpr(cond, prec)
} else if unary, ok := i.(*js.UnaryExpr); ok {
i = optimizeUnaryExpr(unary, prec)
}

switch expr := i.(type) {
case *js.Var:
for expr.Link != nil {
Expand Down Expand Up @@ -1070,6 +1076,9 @@ func (m *jsMinifier) minifyExpr(i js.IExpr, prec js.OpPrec) {
m.write(dotBytes)
m.write(expr.Y.Data)
case *js.GroupExpr:
if cond, ok := expr.X.(*js.CondExpr); ok {
expr.X = m.optimizeCondExpr(cond, js.OpExpr)
}
precInside := exprPrec(expr.X)
if prec <= precInside || precInside == js.OpCoalesce && prec == js.OpBitOr {
m.minifyExpr(expr.X, prec)
Expand Down Expand Up @@ -1208,101 +1217,11 @@ func (m *jsMinifier) minifyExpr(i js.IExpr, prec js.OpPrec) {
m.write(closeBracketBytes)
m.inFor = parentInFor
case *js.CondExpr:
// remove double negative !! in condition, or switch cases for single negative !
if unary1, ok := expr.Cond.(*js.UnaryExpr); ok && unary1.Op == js.NotToken {
if unary2, ok := unary1.X.(*js.UnaryExpr); ok && unary2.Op == js.NotToken {
if isBooleanExpr(unary2.X) {
expr.Cond = unary2.X
}
} else {
expr.Cond = unary1.X
expr.X, expr.Y = expr.Y, expr.X
}
}

finalCond := finalExpr(expr.Cond)
if truthy, ok := isTruthy(expr.Cond); truthy && ok {
// if condition is truthy
m.minifyExpr(expr.X, prec)
} else if !truthy && ok {
// if condition is falsy
m.minifyExpr(expr.Y, prec)
} else if isEqualExpr(finalCond, expr.X) && (exprPrec(finalCond) < js.OpAssign || binaryLeftPrecMap[js.OrToken] <= exprPrec(finalCond)) && (exprPrec(expr.Y) < js.OpAssign || binaryRightPrecMap[js.OrToken] <= exprPrec(expr.Y)) {
// if condition is equal to true body
// for higher prec we need to add group parenthesis, and for lower prec we have parenthesis anyways. This only is shorter if len(expr.X) >= 3. isEqualExpr only checks for literal variables, which is a name will be minified to a one or two character name.
m.minifyExpr(&js.BinaryExpr{js.OrToken, groupExpr(expr.Cond, binaryLeftPrecMap[js.OrToken]), expr.Y}, prec)
} else if isEqualExpr(finalCond, expr.Y) && (exprPrec(finalCond) < js.OpAssign || binaryLeftPrecMap[js.AndToken] <= exprPrec(finalCond)) && (exprPrec(expr.X) < js.OpAssign || binaryRightPrecMap[js.AndToken] <= exprPrec(expr.X)) {
// if condition is equal to false body
// for higher prec we need to add group parenthesis, and for lower prec we have parenthesis anyways. This only is shorter if len(expr.X) >= 3. isEqualExpr only checks for literal variables, which is a name will be minified to a one or two character name.
m.minifyExpr(&js.BinaryExpr{js.AndToken, groupExpr(expr.Cond, binaryLeftPrecMap[js.AndToken]), expr.X}, prec)
} else if isEqualExpr(expr.X, expr.Y) {
// if true and false bodies are equal
if prec <= js.OpExpr {
m.minifyExpr(expr.Cond, binaryLeftPrecMap[js.CommaToken])
m.write(commaBytes)
m.minifyExpr(expr.X, binaryRightPrecMap[js.CommaToken])
} else {
m.write(openParenBytes)
m.minifyExpr(expr.Cond, binaryLeftPrecMap[js.CommaToken])
m.write(commaBytes)
m.minifyExpr(expr.X, binaryRightPrecMap[js.CommaToken])
m.write(closeParenBytes)
}
} else if left, right, ok := toNullishExpr(expr); ok && !m.o.NoNullishOperator {
// no need to check whether left/right need to add groups, as the space saving is always more
m.minifyExpr(groupExpr(left, binaryLeftPrecMap[js.NullishToken]), binaryLeftPrecMap[js.NullishToken])
m.write(nullishBytes)
m.minifyExpr(groupExpr(right, binaryRightPrecMap[js.NullishToken]), binaryRightPrecMap[js.NullishToken])
} else {
// shorten when true and false bodies are true and false
trueX, falseX := isTrue(expr.X), isFalse(expr.X)
trueY, falseY := isTrue(expr.Y), isFalse(expr.Y)
if trueX && falseY || falseX && trueY {
m.minifyBooleanExpr(expr.Cond, falseX, prec)
} else if trueX || trueY {
// trueX != trueY
m.minifyBooleanExpr(expr.Cond, trueY, binaryLeftPrecMap[js.OrToken])
m.write(orBytes)
if trueY {
m.minifyExpr(&js.GroupExpr{X: expr.X}, binaryRightPrecMap[js.OrToken])
} else {
m.minifyExpr(&js.GroupExpr{X: expr.Y}, binaryRightPrecMap[js.OrToken])
}
} else if falseX || falseY {
// falseX != falseY
m.minifyBooleanExpr(expr.Cond, falseX, binaryLeftPrecMap[js.AndToken])
m.write(andBytes)
if falseX {
m.minifyExpr(&js.GroupExpr{X: expr.Y}, binaryRightPrecMap[js.AndToken])
} else {
m.minifyExpr(&js.GroupExpr{X: expr.X}, binaryRightPrecMap[js.AndToken])
}
} else if condExpr, ok := expr.X.(*js.CondExpr); ok && isEqualExpr(expr.Y, condExpr.Y) {
// nested conditional expression with same false bodies
m.minifyExpr(&js.GroupExpr{X: expr.Cond}, binaryLeftPrecMap[js.AndToken])
m.write(andBytes)
m.minifyExpr(&js.GroupExpr{X: condExpr.Cond}, binaryRightPrecMap[js.AndToken])
m.write(questionBytes)
m.minifyExpr(condExpr.X, js.OpAssign)
m.write(colonBytes)
m.minifyExpr(expr.Y, js.OpAssign)
} else {
// regular conditional expression
// convert (a,b)?c:d => a,b?c:d
if prec <= js.OpExpr {
if group, ok := expr.Cond.(*js.GroupExpr); ok {
if comma, ok := group.X.(*js.CommaExpr); ok && js.OpCoalesce <= exprPrec(comma.List[len(comma.List)-1]) {
expr.Cond = group.X
}
}
}
m.minifyExpr(expr.Cond, js.OpCoalesce)
m.write(questionBytes)
m.minifyExpr(expr.X, js.OpAssign)
m.write(colonBytes)
m.minifyExpr(expr.Y, js.OpAssign)
}
}
m.minifyExpr(expr.Cond, js.OpCoalesce)
m.write(questionBytes)
m.minifyExpr(expr.X, js.OpAssign)
m.write(colonBytes)
m.minifyExpr(expr.Y, js.OpAssign)
case *js.OptChainExpr:
m.minifyExpr(expr.X, js.OpCall)
m.write(optChainBytes)
Expand Down
10 changes: 9 additions & 1 deletion js/js_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ func TestJS(t *testing.T) {
{`if(a){if(b)c;else d}else{d}`, `a&&b?c:d`},
{`if(a){if(b)c;else false}else{d}`, `a?!!b&&c:d`},
{`if(a){if(b)c;else d}else{false}`, `!!a&&(b?c:d)`},
{`if(a){if(b)c;else false}else{false}`, `!!a&&(!!b&&c)`}, // could remove group
{`if(a){if(b)c;else false}else{false}`, `!!a&&!!b&&c`},
{`if(a)return a;else return b`, `return a||b`},
{`if(a)return a;else a++`, `if(a)return a;a++`},
{`if(a)return b;else a++`, `if(a)return b;a++`},
Expand Down Expand Up @@ -259,6 +259,7 @@ func TestJS(t *testing.T) {
{`if(a,b)b`, `a,b&&b`},
{`if(a,b)b;else d`, `a,b||d`},
{`if(a=b)a;else b`, `(a=b)||b`},
{`if(!a&&!b){return true}else if(!a||!b){return false}return c&&d`, `return!a&&!b||!(!a||!b)&&c&&d`},

// var declarations
{`var a;var b;a,b`, `var a,b;a,b`},
Expand Down Expand Up @@ -595,6 +596,12 @@ func TestJS(t *testing.T) {
{`a??=b`, `a??=b`},
{`a==false`, `a==!1`},
{`a===false`, `a===!1`},
{`!(a||b)`, `!a&&!b`},
{`!(a&&b)`, `!a||!b`},
//{`!(!a||!b)`, `a&&b`}, // we don't know of a or b are booleans
//{`!(!a&&!b)`, `a||b`},
//{`!(!a&&b)&&c`, `(a||!b)&&c`},
{`!(a&&b)&&c`, `!(a&&b)&&c`},

// other
{`async function g(){await x+y}`, `async function g(){await x+y}`},
Expand Down Expand Up @@ -693,6 +700,7 @@ func TestJS(t *testing.T) {
{`return a,b,void 0`, `return a,b`},
{`var arr=[];var slice=arr.slice;var concat=arr.concat;var push=arr.push;var indexOf=arr.indexOf;var class2type={};`, `var arr=[],slice=arr.slice,concat=arr.concat,push=arr.push,indexOf=arr.indexOf,class2type={}`},
{`var arr=[];var class2type={};a=5;var rlocalProtocol=0`, `var arr=[],class2type={};a=5;var rlocalProtocol=0`},
{`a=b;if(!o)return c;return d`, `return a=b,o?d:c`},

// bugs
{`({"":a})`, `({"":a})`}, // go-fuzz
Expand Down
125 changes: 118 additions & 7 deletions js/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,14 @@ func groupExpr(i js.IExpr, prec js.OpPrec) js.IExpr {

// TODO: use in more cases
func condExpr(cond, x, y js.IExpr) js.IExpr {
if comma, ok := cond.(*js.CommaExpr); ok {
comma.List[len(comma.List)-1] = &js.CondExpr{
Cond: groupExpr(comma.List[len(comma.List)-1], js.OpCoalesce),
X: groupExpr(x, js.OpAssign),
Y: groupExpr(y, js.OpAssign),
}
return comma
}
return &js.CondExpr{
Cond: groupExpr(cond, js.OpCoalesce),
X: groupExpr(x, js.OpAssign),
Expand Down Expand Up @@ -541,6 +549,9 @@ func isBooleanExpr(expr js.IExpr) bool {
return unaryExpr.Op == js.NotToken
} else if binaryExpr, ok := expr.(*js.BinaryExpr); ok {
op := binaryOpPrecMap[binaryExpr.Op]
if op == js.OpAnd || op == js.OpOr {
return isBooleanExpr(binaryExpr.X) && isBooleanExpr(binaryExpr.Y)
}
return op == js.OpCompare || op == js.OpEquals
} else if litExpr, ok := expr.(*js.LiteralExpr); ok {
return litExpr.TokenType == js.TrueToken || litExpr.TokenType == js.FalseToken
Expand All @@ -550,7 +561,7 @@ func isBooleanExpr(expr js.IExpr) bool {
return false
}

func (m *jsMinifier) minifyBooleanExpr(expr js.IExpr, invert bool, prec js.OpPrec) {
func optimizeBooleanExpr(expr js.IExpr, invert bool, prec js.OpPrec) js.IExpr {
if invert {
// unary !(boolean) has already been handled
if binaryExpr, ok := expr.(*js.BinaryExpr); ok && binaryOpPrecMap[binaryExpr.Op] == js.OpEquals {
Expand All @@ -563,19 +574,119 @@ func (m *jsMinifier) minifyBooleanExpr(expr js.IExpr, invert bool, prec js.OpPre
} else if binaryExpr.Op == js.NotEqEqToken {
binaryExpr.Op = js.EqEqEqToken
}
m.minifyExpr(expr, prec)
return expr
} else {
m.write(notBytes)
m.minifyExpr(&js.GroupExpr{X: expr}, js.OpUnary)
return &js.UnaryExpr{js.NotToken, groupExpr(expr, js.OpUnary)}
}
} else if isBooleanExpr(expr) {
m.minifyExpr(&js.GroupExpr{X: expr}, prec)
return groupExpr(expr, prec)
} else {
m.write(notNotBytes)
m.minifyExpr(&js.GroupExpr{X: expr}, js.OpUnary)
return &js.UnaryExpr{js.NotToken, &js.UnaryExpr{js.NotToken, groupExpr(expr, js.OpUnary)}}
}
}

func optimizeUnaryExpr(expr *js.UnaryExpr, prec js.OpPrec) js.IExpr {
// rewrite !(a||b) to !a&&!b and similar conversions
if group, ok := expr.X.(*js.GroupExpr); ok && expr.Op == js.NotToken {
if binary, ok := group.X.(*js.BinaryExpr); ok && (binary.Op == js.AndToken || binary.Op == js.OrToken) {
op := js.AndToken
if binary.Op == js.AndToken {
op = js.OrToken
}
unaryX, notX := binary.X.(*js.UnaryExpr)
if notX {
notX = unaryX.Op == js.NotToken
}
unaryY, notY := binary.Y.(*js.UnaryExpr)
if notY {
notY = unaryY.Op == js.NotToken
}
precInside := binaryOpPrecMap[op]
if (prec <= precInside || precInside == js.OpCoalesce && prec == js.OpBitOr) && !notX && !notY {
binary.Op = op
binary.X = &js.UnaryExpr{js.NotToken, binary.X}
binary.Y = &js.UnaryExpr{js.NotToken, binary.Y}
return binary
}
}
}
return expr
}

func (m *jsMinifier) optimizeCondExpr(expr *js.CondExpr, prec js.OpPrec) js.IExpr {
// remove double negative !! in condition, or switch cases for single negative !
if unary1, ok := expr.Cond.(*js.UnaryExpr); ok && unary1.Op == js.NotToken {
if unary2, ok := unary1.X.(*js.UnaryExpr); ok && unary2.Op == js.NotToken {
if isBooleanExpr(unary2.X) {
expr.Cond = unary2.X
}
} else {
expr.Cond = unary1.X
expr.X, expr.Y = expr.Y, expr.X
}
}

finalCond := finalExpr(expr.Cond)
if truthy, ok := isTruthy(expr.Cond); truthy && ok {
// if condition is truthy
return expr.X
} else if !truthy && ok {
// if condition is falsy
return expr.Y
} else if isEqualExpr(finalCond, expr.X) && (exprPrec(finalCond) < js.OpAssign || binaryLeftPrecMap[js.OrToken] <= exprPrec(finalCond)) && (exprPrec(expr.Y) < js.OpAssign || binaryRightPrecMap[js.OrToken] <= exprPrec(expr.Y)) {
// if condition is equal to true body
// for higher prec we need to add group parenthesis, and for lower prec we have parenthesis anyways. This only is shorter if len(expr.X) >= 3. isEqualExpr only checks for literal variables, which is a name will be minified to a one or two character name.
return &js.BinaryExpr{js.OrToken, groupExpr(expr.Cond, binaryLeftPrecMap[js.OrToken]), expr.Y}
} else if isEqualExpr(finalCond, expr.Y) && (exprPrec(finalCond) < js.OpAssign || binaryLeftPrecMap[js.AndToken] <= exprPrec(finalCond)) && (exprPrec(expr.X) < js.OpAssign || binaryRightPrecMap[js.AndToken] <= exprPrec(expr.X)) {
// if condition is equal to false body
// for higher prec we need to add group parenthesis, and for lower prec we have parenthesis anyways. This only is shorter if len(expr.X) >= 3. isEqualExpr only checks for literal variables, which is a name will be minified to a one or two character name.
return &js.BinaryExpr{js.AndToken, groupExpr(expr.Cond, binaryLeftPrecMap[js.AndToken]), expr.X}
} else if isEqualExpr(expr.X, expr.Y) {
// if true and false bodies are equal
return groupExpr(&js.CommaExpr{[]js.IExpr{expr.Cond, expr.X}}, prec)
} else if left, right, ok := toNullishExpr(expr); ok && !m.o.NoNullishOperator {
// no need to check whether left/right need to add groups, as the space saving is always more
return &js.BinaryExpr{js.NullishToken, groupExpr(left, binaryLeftPrecMap[js.NullishToken]), groupExpr(right, binaryRightPrecMap[js.NullishToken])}
} else {
// shorten when true and false bodies are true and false
trueX, falseX := isTrue(expr.X), isFalse(expr.X)
trueY, falseY := isTrue(expr.Y), isFalse(expr.Y)
if trueX && falseY || falseX && trueY {
return optimizeBooleanExpr(expr.Cond, falseX, prec)
} else if trueX || trueY {
// trueX != trueY
cond := optimizeBooleanExpr(expr.Cond, trueY, binaryLeftPrecMap[js.OrToken])
if trueY {
return &js.BinaryExpr{js.OrToken, cond, groupExpr(expr.X, binaryRightPrecMap[js.OrToken])}
} else {
return &js.BinaryExpr{js.OrToken, cond, groupExpr(expr.Y, binaryRightPrecMap[js.OrToken])}
}
} else if falseX || falseY {
// falseX != falseY
cond := optimizeBooleanExpr(expr.Cond, falseX, binaryLeftPrecMap[js.AndToken])
if falseX {
return &js.BinaryExpr{js.AndToken, cond, groupExpr(expr.Y, binaryRightPrecMap[js.AndToken])}
} else {
return &js.BinaryExpr{js.AndToken, cond, groupExpr(expr.X, binaryRightPrecMap[js.AndToken])}
}
} else if condExpr, ok := expr.X.(*js.CondExpr); ok && isEqualExpr(expr.Y, condExpr.Y) {
// nested conditional expression with same false bodies
return &js.CondExpr{&js.BinaryExpr{js.AndToken, groupExpr(expr.Cond, binaryLeftPrecMap[js.AndToken]), groupExpr(condExpr.Cond, binaryRightPrecMap[js.AndToken])}, condExpr.X, expr.Y}
} else if prec <= js.OpExpr {
// regular conditional expression
// convert (a,b)?c:d => a,b?c:d
if group, ok := expr.Cond.(*js.GroupExpr); ok {
if comma, ok := group.X.(*js.CommaExpr); ok && js.OpCoalesce <= exprPrec(comma.List[len(comma.List)-1]) {
expr.Cond = comma.List[len(comma.List)-1]
comma.List[len(comma.List)-1] = expr
return comma
}
}
}
}
return expr
}

func endsInIf(istmt js.IStmt) bool {
switch stmt := istmt.(type) {
case *js.IfStmt:
Expand Down

0 comments on commit a2eb81a

Please sign in to comment.