Skip to content

Commit 886adb5

Browse files
committed
Avoid excess breaks within switch (fix jashkenas#5344)
Add new `alwaysJumps` node method to detect when a switch case can safely drop its `break`. Similar to `jumps` but ANDing instead of OR.
1 parent 07f644c commit 886adb5

File tree

4 files changed

+155
-15
lines changed

4 files changed

+155
-15
lines changed

lib/coffeescript/nodes.js

Lines changed: 91 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/coffeescript/rewriter.js

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/nodes.coffee

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -393,12 +393,18 @@ exports.Base = class Base
393393
includeCommentFragments: NO
394394

395395
# `jumps` tells you if an expression, or an internal part of an expression
396-
# has a flow control construct (like `break`, or `continue`, or `return`,
397-
# or `throw`) that jumps out of the normal flow of control and can’t be
398-
# used as a value. This is important because things like this make no sense;
399-
# we have to disallow them.
396+
# has a flow control construct (like `break`, `continue`, or `return`)
397+
# that jumps out of the normal flow of control and can’t be used as a value.
398+
# This is important because things like this make no sense;
399+
# we have to disallow them. Note that `throw` *is* allowed here though.
400400
jumps: NO
401401

402+
# `alwaysJumps` tells you whether this node *always* has a flow control
403+
# construct (like `break`, `continue`, `return`, or `throw`) that jumps out
404+
# of the normal flow of control, so that the immediately following node
405+
# definitely won't execute.
406+
alwaysJumps: NO
407+
402408
# If `node.shouldCache() is false`, it is safe to use `node` more than once.
403409
# Otherwise you need to store the value of `node` in a variable and output
404410
# that variable several times instead. Kind of like this: `5` need not be
@@ -600,6 +606,13 @@ exports.Block = class Block extends Base
600606
for exp in @expressions
601607
return jumpNode if jumpNode = exp.jumps o
602608

609+
# Block is executed in sequence, so if any statement always jumps,
610+
# then so does the block.
611+
alwaysJumps: (o) ->
612+
for exp in @expressions
613+
return yes if exp.alwaysJumps o
614+
no
615+
603616
# A Block node does not return its entire body, rather it
604617
# ensures that the final expression is returned.
605618
makeReturn: (results, mark) ->
@@ -1198,6 +1211,8 @@ exports.StatementLiteral = class StatementLiteral extends Literal
11981211
return this if @value is 'break' and not (o?.loop or o?.block)
11991212
return this if @value is 'continue' and not o?.loop
12001213

1214+
alwaysJumps: (o) -> Boolean @jumps o
1215+
12011216
compileNode: (o) ->
12021217
[@makeCode "#{@tab}#{@value};"]
12031218

@@ -1269,6 +1284,7 @@ exports.Return = class Return extends Base
12691284
isStatement: YES
12701285
makeReturn: THIS
12711286
jumps: THIS
1287+
alwaysJumps: YES
12721288

12731289
compileToFragments: (o, level) ->
12741290
expr = @expression?.makeReturn()
@@ -1398,6 +1414,7 @@ exports.Value = class Value extends Base
13981414
isJSXTag : -> @base instanceof JSXTag
13991415
assigns : (name) -> not @properties.length and @base.assigns name
14001416
jumps : (o) -> not @properties.length and @base.jumps o
1417+
alwaysJumps : (o) -> not @properties.length and @base.alwaysJumps o
14011418

14021419
isObject: (onlyGenerated) ->
14031420
return no if @properties.length
@@ -3211,6 +3228,7 @@ exports.ModuleDeclaration = class ModuleDeclaration extends Base
32113228

32123229
isStatement: YES
32133230
jumps: THIS
3231+
alwaysJumps: NO
32143232
makeReturn: THIS
32153233

32163234
checkSource: ->
@@ -3896,6 +3914,7 @@ exports.Code = class Code extends Base
38963914
isStatement: -> @isMethod
38973915

38983916
jumps: NO
3917+
alwaysJumps: NO
38993918

39003919
makeScope: (parentScope) -> new Scope parentScope, @body, this
39013920

@@ -4555,6 +4574,13 @@ exports.While = class While extends Base
45554574
return jumpNode if jumpNode = node.jumps loop: yes
45564575
no
45574576

4577+
alwaysJumps: ->
4578+
{expressions} = @body
4579+
return no unless expressions.length
4580+
for node in expressions
4581+
return yes if node.alwaysJumps loop: yes
4582+
no
4583+
45584584
# The main difference from a JavaScript *while* is that the CoffeeScript
45594585
# *while* can be used as a part of a larger expression -- while loops may
45604586
# return an array containing the computed result of each iteration.
@@ -4931,6 +4957,10 @@ exports.Try = class Try extends Base
49314957

49324958
jumps: (o) -> @attempt.jumps(o) or @catch?.jumps(o)
49334959

4960+
alwaysJumps: (o) ->
4961+
(@attempt.alwaysJumps(o) and @catch?.alwaysJumps(o)) or
4962+
@ensure?.alwaysJumps(o)
4963+
49344964
makeReturn: (results, mark) ->
49354965
if mark
49364966
@attempt?.makeReturn results, mark
@@ -4988,7 +5018,9 @@ exports.Catch = class Catch extends Base
49885018

49895019
isStatement: YES
49905020

4991-
jumps: (o) -> @recovery.jumps(o)
5021+
jumps: (o) -> @recovery.jumps o
5022+
5023+
alwaysJumps: (o) -> @recovery.alwaysJumps o
49925024

49935025
makeReturn: (results, mark) ->
49945026
ret = @recovery.makeReturn results, mark
@@ -5037,6 +5069,7 @@ exports.Throw = class Throw extends Base
50375069

50385070
isStatement: YES
50395071
jumps: NO
5072+
alwaysJumps: YES
50405073

50415074
# A **Throw** is already a return, of sorts...
50425075
makeReturn: THIS
@@ -5498,6 +5531,12 @@ exports.Switch = class Switch extends Base
54985531
return jumpNode if jumpNode = block.jumps o
54995532
@otherwise?.jumps o
55005533

5534+
alwaysJumps: (o = {block: yes}) ->
5535+
return no unless @cases.length
5536+
for {block} in @cases
5537+
return no unless block.alwaysJumps o
5538+
yes
5539+
55015540
makeReturn: (results, mark) ->
55025541
block.makeReturn(results, mark) for {block} in @cases
55035542
@otherwise or= new Block [new Literal 'void 0'] if results
@@ -5516,8 +5555,8 @@ exports.Switch = class Switch extends Base
55165555
fragments = fragments.concat @makeCode(idt1 + "case "), cond.compileToFragments(o, LEVEL_PAREN), @makeCode(":\n")
55175556
fragments = fragments.concat body, @makeCode('\n') if (body = block.compileToFragments o, LEVEL_TOP).length > 0
55185557
break if i is @cases.length - 1 and not @otherwise
5519-
expr = @lastNode block.expressions
5520-
continue if expr instanceof Return or expr instanceof Throw or (expr instanceof Literal and expr.jumps() and expr.value isnt 'debugger')
5558+
#expr = @lastNode block.expressions
5559+
continue if block.alwaysJumps()
55215560
fragments.push cond.makeCode(idt2 + 'break;\n')
55225561
if @otherwise and @otherwise.expressions.length
55235562
fragments.push @makeCode(idt1 + "default:\n"), (@otherwise.compileToFragments o, LEVEL_TOP)..., @makeCode("\n")
@@ -5615,6 +5654,8 @@ exports.If = class If extends Base
56155654

56165655
jumps: (o) -> @body.jumps(o) or @elseBody?.jumps(o)
56175656

5657+
alwaysJumps: (o) -> @body.alwaysJumps(o) and @elseBody?.alwaysJumps(o)
5658+
56185659
compileNode: (o) ->
56195660
if @isStatement o then @compileStatement o else @compileExpression o
56205661

test/control_flow.coffee

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,22 @@ test "Issue #997. Switch doesn't fallthrough.", ->
440440

441441
eq val, 1
442442

443+
test "Issue #5344. Switch doesn't generate unnecessary break statements.", ->
444+
js = CoffeeScript.compile '''
445+
identify = (n) ->
446+
switch n
447+
when 1, 2, 3, 4, 5
448+
if n % 2 == 0
449+
'small even'
450+
else
451+
'small odd'
452+
when 0
453+
'perfect'
454+
else
455+
null
456+
'''
457+
ok not js.includes 'break'
458+
443459
# Throw
444460

445461
test "Throw should be usable as an expression.", ->

0 commit comments

Comments
 (0)