Skip to content

Commit f4e9d19

Browse files
committed
switch case liveness: default is always last
1 parent 7aa47c3 commit f4e9d19

File tree

3 files changed

+105
-88
lines changed

3 files changed

+105
-88
lines changed

internal/bundler_tests/bundler_dce_test.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -1449,14 +1449,18 @@ func TestDeadCodeInsideUnusedCases(t *testing.T) {
14491449
}
14501450
14511451
// Check for "default"
1452+
switch (0) {
1453+
case 1: _ = require('./FAIL-default-1'); break
1454+
default: _ = require('./a'); break
1455+
}
14521456
switch (1) {
14531457
case 1: _ = require('./a'); break
14541458
default: _ = require('./FAIL-default'); break
14551459
}
14561460
switch (0) {
14571461
case 1: _ = require('./FAIL-default-1'); break
1458-
default: _ = require('./a'); break
1459-
case 0: _ = require('./FAIL-default-0'); break
1462+
default: _ = require('./FAIL-default'); break
1463+
case 0: _ = require('./a'); break
14601464
}
14611465
14621466
// Check for non-constant cases

internal/bundler_tests/snapshots/snapshots_dce.txt

+10-2
Original file line numberDiff line numberDiff line change
@@ -968,6 +968,14 @@ switch (1) {
968968
_ = null;
969969
break;
970970
}
971+
switch (0) {
972+
case 1:
973+
_ = null;
974+
break;
975+
default:
976+
_ = require_a();
977+
break;
978+
}
971979
switch (1) {
972980
case 1:
973981
_ = require_a();
@@ -981,10 +989,10 @@ switch (0) {
981989
_ = null;
982990
break;
983991
default:
984-
_ = require_a();
992+
_ = null;
985993
break;
986994
case 0:
987-
_ = null;
995+
_ = require_a();
988996
break;
989997
}
990998
switch (1) {

internal/js_parser/js_parser.go

+89-84
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ type parser struct {
5959
declaredSymbols []js_ast.DeclaredSymbol
6060
globPatternImports []globPatternImport
6161
runtimeImports map[string]ast.LocRef
62-
deadCaseChecker deadCaseChecker
6362
duplicateCaseChecker duplicateCaseChecker
6463
unrepresentableIdentifiers map[string]bool
6564
legacyOctalLiterals map[js_ast.E]logger.Range
@@ -748,84 +747,87 @@ type fnOnlyDataVisit struct {
748747
silenceMessageAboutThisBeingUndefined bool
749748
}
750749

751-
type livenessStatus uint8
750+
type livenessStatus int8
752751

753752
const (
754-
livenessUnknown livenessStatus = iota
755-
alwaysDead
756-
alwaysLive
753+
alwaysDead livenessStatus = -1
754+
livenessUnknown livenessStatus = 0
755+
alwaysLive livenessStatus = 1
757756
)
758757

759-
type deadCaseChecker struct {
760-
test js_ast.E
761-
earlierCaseWasMaybeTaken bool
762-
furtherCasesAreDead bool
763-
mayHaveFallenThrough bool
758+
type switchCaseLiveness struct {
759+
status livenessStatus
760+
canFallThrough bool
764761
}
765762

766-
func (dc *deadCaseChecker) reset(p *parser, test js_ast.E) {
767-
*dc = deadCaseChecker{
768-
test: test,
769-
furtherCasesAreDead: p.isControlFlowDead,
770-
}
771-
}
772-
773-
func (dc *deadCaseChecker) checkCase(c js_ast.Case) (status livenessStatus) {
774-
if dc.furtherCasesAreDead {
775-
return alwaysDead
776-
}
763+
func analyzeSwitchCasesForLiveness(s *js_ast.SSwitch) []switchCaseLiveness {
764+
cases := make([]switchCaseLiveness, 0, len(s.Cases))
765+
defaultIndex := -1
766+
767+
// Determine the status of the individual cases independently
768+
maxStatus := alwaysDead
769+
for i, c := range s.Cases {
770+
if c.ValueOrNil.Data == nil {
771+
defaultIndex = i
772+
}
773+
774+
// Check the value for strict equality
775+
var status livenessStatus
776+
if maxStatus == alwaysLive {
777+
status = alwaysDead // Everything after an always-live case is always dead
778+
} else if c.ValueOrNil.Data == nil {
779+
status = alwaysDead // This is the default case, and will be filled in later
780+
} else if isEqualToTest, ok := js_ast.CheckEqualityIfNoSideEffects(s.Test.Data, c.ValueOrNil.Data, js_ast.StrictEquality); ok {
781+
if isEqualToTest {
782+
status = alwaysLive // This branch will always be matched, and will be taken unless an earlier branch was taken
783+
} else {
784+
status = alwaysDead // This branch will never be matched, and will not be taken unless there was fall-through
785+
}
786+
} else {
787+
status = livenessUnknown // This branch depends on run-time values and may or may not be matched
788+
}
789+
if maxStatus < status {
790+
maxStatus = status
791+
}
777792

778-
// Check for strict equality
779-
var isEqualToTest bool
780-
var isEqualityKnown bool
781-
if c.ValueOrNil.Data != nil {
782-
// Non-default case
783-
isEqualToTest, isEqualityKnown = js_ast.CheckEqualityIfNoSideEffects(dc.test, c.ValueOrNil.Data, js_ast.StrictEquality)
784-
} else {
785-
// Default case
786-
if !dc.earlierCaseWasMaybeTaken {
787-
isEqualToTest = true
788-
isEqualityKnown = true
793+
// Check for potential fall-through by checking for a jump at the end of the body
794+
canFallThrough := true
795+
stmts := c.Body
796+
for len(stmts) > 0 {
797+
switch s := stmts[len(stmts)-1].Data.(type) {
798+
case *js_ast.SBlock:
799+
stmts = s.Stmts // If this ends with a block, check the block's body next
800+
continue
801+
case *js_ast.SBreak, *js_ast.SContinue, *js_ast.SReturn, *js_ast.SThrow:
802+
canFallThrough = false
803+
}
804+
break
789805
}
806+
807+
cases = append(cases, switchCaseLiveness{
808+
status: status,
809+
canFallThrough: canFallThrough,
810+
})
790811
}
791812

792-
// Check for potential fall-through by checking for a jump at the end of the body
793-
canFallThrough := true
794-
stmts := c.Body
795-
for len(stmts) > 0 {
796-
switch s := stmts[len(stmts)-1].Data.(type) {
797-
case *js_ast.SBlock:
798-
stmts = s.Stmts // If this ends with a block, check the block's body next
799-
continue
800-
case *js_ast.SBreak, *js_ast.SContinue, *js_ast.SReturn, *js_ast.SThrow:
801-
canFallThrough = false
802-
}
803-
break
813+
// Set the liveness for the default case last based on the other cases
814+
if defaultIndex != -1 {
815+
// The negation here transposes "always live" with "always dead"
816+
cases[defaultIndex].status = -maxStatus
804817
}
805818

806-
// Update the state machine
807-
if isEqualityKnown {
808-
if isEqualToTest {
809-
// This branch will always be matched, and will be taken unless an earlier branch was taken
810-
if !dc.earlierCaseWasMaybeTaken {
811-
status = alwaysLive
812-
}
813-
if !canFallThrough {
814-
dc.furtherCasesAreDead = true
815-
}
816-
dc.earlierCaseWasMaybeTaken = true
817-
} else {
818-
// This branch will never be matched, and will not be taken unless there was fall-through
819-
if !dc.mayHaveFallenThrough {
820-
status = alwaysDead
819+
// Then propagate fall-through information in linear fall-through order
820+
for i, c := range cases {
821+
// Propagate state forward if this isn't dead. Note that the "can fall
822+
// through" flag does not imply "must fall through". The body may have
823+
// an embedded "break" inside an if statement, for example.
824+
if c.status != alwaysDead {
825+
for j := i + 1; j < len(cases) && cases[j-1].canFallThrough; j++ {
826+
cases[j].status = livenessUnknown
821827
}
822828
}
823-
} else {
824-
// This branch depends on run-time values and may or may not be matched
825-
dc.earlierCaseWasMaybeTaken = true
826829
}
827-
dc.mayHaveFallenThrough = canFallThrough && status != alwaysDead
828-
return
830+
return cases
829831
}
830832

831833
const bloomFilterSize = 251
@@ -10959,23 +10961,36 @@ func (p *parser) visitAndAppendStmt(stmts []js_ast.Stmt, stmt js_ast.Stmt) []js_
1095910961
p.pushScopeForVisitPass(js_ast.ScopeBlock, s.BodyLoc)
1096010962
oldIsInsideSwitch := p.fnOrArrowDataVisit.isInsideSwitch
1096110963
p.fnOrArrowDataVisit.isInsideSwitch = true
10962-
p.deadCaseChecker.reset(p, s.Test.Data)
10963-
end := 0
10964-
for _, c := range s.Cases {
10965-
// Visit the value for non-default cases
10966-
var status livenessStatus
10964+
10965+
// Visit case values first
10966+
for i := range s.Cases {
10967+
c := &s.Cases[i]
1096710968
if c.ValueOrNil.Data != nil {
1096810969
c.ValueOrNil = p.visitExpr(c.ValueOrNil)
10969-
status = p.deadCaseChecker.checkCase(c)
1097010970
p.warnAboutEqualityCheck("case", c.ValueOrNil, c.ValueOrNil.Loc)
1097110971
p.warnAboutTypeofAndString(s.Test, c.ValueOrNil, onlyCheckOriginalOrder)
10972-
} else {
10973-
status = p.deadCaseChecker.checkCase(c)
1097410972
}
10973+
}
10974+
10975+
// Check for duplicate case values
10976+
p.duplicateCaseChecker.reset()
10977+
for _, c := range s.Cases {
10978+
if c.ValueOrNil.Data != nil {
10979+
p.duplicateCaseChecker.check(p, c.ValueOrNil)
10980+
}
10981+
}
10982+
10983+
// Then analyze the cases to determine which ones are live and/or dead
10984+
cases := analyzeSwitchCasesForLiveness(s)
10985+
10986+
// Then visit case bodies, and potentially filter out dead cases
10987+
end := 0
10988+
for i, c := range s.Cases {
10989+
isAlwaysDead := cases[i].status == alwaysDead
1097510990

1097610991
// Potentially treat the case body as dead code
1097710992
old := p.isControlFlowDead
10978-
if status == alwaysDead {
10993+
if isAlwaysDead {
1097910994
p.isControlFlowDead = true
1098010995
}
1098110996
c.Body = p.visitStmts(c.Body, stmtsSwitch)
@@ -10986,29 +11001,19 @@ func (p *parser) visitAndAppendStmt(stmts []js_ast.Stmt, stmt js_ast.Stmt) []js_
1098611001
// removed safely, so if the body isn't empty then that means it contains
1098711002
// some statements that can't be removed safely (e.g. a hoisted "var").
1098811003
// So don't remove this case if the body isn't empty.
10989-
if p.options.minifySyntax && status == alwaysDead && len(c.Body) == 0 {
11004+
if p.options.minifySyntax && isAlwaysDead && len(c.Body) == 0 {
1099011005
continue
1099111006
}
1099211007

1099311008
// Make sure the assignment to the body above is preserved
1099411009
s.Cases[end] = c
1099511010
end++
1099611011
}
10997-
10998-
// Filter out all removed cases
1099911012
s.Cases = s.Cases[:end]
1100011013

1100111014
p.fnOrArrowDataVisit.isInsideSwitch = oldIsInsideSwitch
1100211015
p.popScope()
1100311016

11004-
// Check for duplicate case values
11005-
p.duplicateCaseChecker.reset()
11006-
for _, c := range s.Cases {
11007-
if c.ValueOrNil.Data != nil {
11008-
p.duplicateCaseChecker.check(p, c.ValueOrNil)
11009-
}
11010-
}
11011-
1101211017
// Unwrap switch statements in dead code
1101311018
if p.options.minifySyntax && p.isControlFlowDead {
1101411019
for _, c := range s.Cases {

0 commit comments

Comments
 (0)