Expand/Shrink

Switch Statement

The switch statement can be used instead of an if-construct that repeatedly tests the same expression, for example:
if    ch=’a’ then   puts(1,"ch is a\n")
elsif ch=’b’ then   puts(1,"ch is b\n")
elsif ch=’c’ then   puts(1,"ch is c\n")
  ...
else                puts(1,"ch is something else\n")
end if
Can, if you prefer, and perhaps with some caution as detailed below, be replaced with:
switch ch
  case ’a’: puts(1,"ch is a\n")
  case ’b’: puts(1,"ch is b\n")
  case ’c’: puts(1,"ch is c\n")
    ...
  default:  puts(1,"ch is something else\n")
end switch
Note that in Phix the choice of which construct to use should be entirely based on personal preference, ease of writing, and ease of future maintenance (not performance).

For instance one thing that may significantly simplify maintenance is that it is trivial to change the second branch of the if-construct to
elsif ch=’b’ and ignoreb=0 then
whereas making the equivalent change to the switch-construct, whereby a 'b' ends up in the default case when ignoreb!=0, is not quite so easy: The clause case 'b' and ignoreb=0: is in fact equivalent to elsif ch=('b' and ignoreb=0) then which is in practice optimised to elsif ch=(ignoreb=0) (since "'b' and" === "true and"). When writing case expressions, you always have to remember that there is an implicit switchvar=( ) around them, so it is treated as ch=('b' and ignoreb=0) rather than the presumably (in this case!) desired (ch='b') and (ignoreb=0). The closest thing would instead be
    case ’b’: if ignoreb=0 then
                puts(1,"ch is b\n")
                break
              end if
              fallthrough
    default:
But, obviously, you can only move one case so that it is immediately above the default. Otherwise you would have to duplicate code, use a flag, or factor out common code (aka the default case here) into a subroutine that can be invoked from several different places.

Conversely you might want to make it deliberately difficult to thwart the creation of a jump table (assuming careful testing has proved that to be significant).

In both the above (first two) snippets, it is the back-end that decides whether a jump table is suitable: experiments revealed that branch misprediction makes <8 branches faster as a daisy-chain of jumps, and >8 branches faster as a jump table, and the implementation is such that both constructs can generate either. A jump table can only be constructed when all tests apply (as they always do in a switch statement) to the same variable, all values are fixed integers, and no complex/compound tests are involved.

In particular, not that it happens by default any longer in Phix, a switch true/false using a jump table was woefully slower than a single test/jump. Also, I saw no reason to waste time converting perfectly valid if-constructs in legacy code to switch statements, especially when doing so could actually make things slower, and introduce bugs/typos, so instead I decided to make the compiler do all the work for me automatically.

A jump table usually causes an AGI stall and almost inevitably incurs a branch misprediction, which flushes the pipeline and costs at least 8 clocks, on some recent/advanced processors even more. Modern processors are also quite astonishingly good at branch prediction, including spotting a whole range of loop patterns, but that occurs (in hardware) at the instruction level, not at jump table entry level. As above, you need about 8 branches to "break even", but the back-end is perfectly happy to make that decision for you, including whether or not a jump table would be "too sparse" (<5% populated), as well as perform any necessary preliminary bounds checking. For practical reasons, a switch construct with no case statements triggers a compilation error ('case expected' on the 'end switch'), in much the same way that you cannot have an if-construct with no conditions.

In short, unlike most other languages, performance should not or at least very rarely (and only after testing/timing both) be a factor in determining which construct to use.

The full syntax of a switch statement is as follows:
    switch <expr> [("with"|"without") "fallthrough"|"fallthru"|"jump_table"] [do]
      {("case" <expr>{","<expr>}|"default"|"else") [":"|"then"] [<block>] ["break"|"fallthrough"|"fallthru"]}
    end switch
Additional "break" statements can also be buried deep inside <block>, with execution resuming at the end switch.
Also note that "exit" and "return" statements terminate the containing loop/routine, without any attention to the location of the end switch.
Other programming languages use/overload break for both switch and loop constructs, whereas in phix they are separate keywords.

The else keyword can be used instead of default, and behaves identically.
Only one default clause is permitted per switch statement, but it can be placed anywhere.
If placed anywhere other than at the end, an error triggers if a jump table cannot be constructed.

Normally each branch rejoins execution at the end switch statement, but you can change that for individual cases or the entire switch construct using either spelling of fallthrough (fallthru is allowed for compatibility with Euphoria). The presence of any fallthrough or the more explicit jump_table option forces the use of a jump table, should you incorrectly care about that. The diligent programmer is expected to avoid switch/fallthrough/jump_table when inappropriate, for instance "with jump_table case 1,1_000_000_000:" would force the compiler to try and create a 4 or 8GB jump table, perhaps with only 2 entries that are actually used, and unable to rely on the <5% rule because of the explicit overriding "with jump_table" directive.

"break" and "fallthough" are effectively polar opposites: in a "switch with fallthrough" you may need the occasional "break"; whereas in a "switch [without fallthrough]" you may need the occasional "fallthrough", and in both cases the other will be implicitly assumed when not explicitly present. One other difference is that a "break" is effectively "goto the end switch", and can occur within nested conditional statements (and may be used to that effect in all forms of the switch statement), whereas "fallthrough" is effectively "omit the usual goto" and is therefore only valid immediately prior to case/default/else.

Duplicate entries in the jump table trigger an error. Note that the same error may also occur on a plain if-construct, should they be detected before anything that prohibits the use of a jump table.

So-called "smart switch" processing is also supported, in other words "back-to-back" cases do not have the usual implied "break":
    case 3:
    case 4: <block>
behaves identically to
    case 3,4: <block>
Be wary of the hidden gotcha in that commenting out the last line of code between two cases can effectively insert a "fallthrough".

It is also possible to write fully "variable/polymorphic" switch statements, eg:
    procedure dosomething(object action, stop, start, term)
      switch action
        case stop: running=false
        case start: running=true
        case term: terminate=true
      end switch
    end procedure

    dosomething(i,1,0,-1)
    ...
    dosomething(j,1,2,3)
    ...
    dosomething("hey",55.37,"NO",{{"this"}})
Obviously when the branch cases are not fixed, or non-integer, it is not possible to construct a jump table, and attempts to fallthrough will trigger an error (you would need to resort to multiple if-constructs with any required additional "or action=").

Of course the reverse may be true: an if-construct which looks completely variable to the front-end may in fact prove to consist entirely of known fixed integer values after the detailed analysis phase, so we can emit a jump table for it. That might be particularly useful for general purpose library code of which your program only exercises a small portion.

Very rarely, the compiler might construct a jump table for an if-construct that proves noticeably less efficient than a daisy-chain of cmp/jmp, specifically when the first few branches account for a very high percentage of cases. Should that happen, one easy solution might be to invert one test, eg "if 'a'=ch then" (with an appropriate comment). You would need to use "p -d" and examine the resulting list.asm to verify the presence/absense of a jump table.

Unlike Euphoria, Phix does not support "with label" on if/for/while/switch constructs, or the break "label" statement.

The default of not needing break statements but instead explicit fallthroughs was, I claim, copied by Go from Phix.
(OK, I’m joking, although chronologically that does actually fit.)

Note that desktop/Phix uses the exit keyword to leave for and while loops, and the break keyword to leave a switch statement, eg
without js
--with javascript_semantics

    for i=1 to 7 do
        switch i do
            case 1: ?{"switch",i}
            case 2: if i=2 then break end if        -- [1] (ok)
                    ?{"switch",i} -- (never printed)
            case 3: while true do
                        if i=3 then exit end if     -- [1] (ok)
                        ?"3" -- (never printed)
                    end while
                    ?{"end while",i}
            case 4: while true do
                        if i=4 then break end if    -- [2] (not js)
                        ?"4" -- (never printed)
                    end while
                    ?{"end while",i} -- (never printed)
            case 5: while true do
                        switch i do
                            case 5: exit            -- [2] (not js)
                            default: ?{"wsw",i} -- (never printed)
                        end switch
                    end while
                    ?{"end while",i}
            case 6: ?{"case",i}
                    if i=6 then exit end if         -- [2] (not js)
                    ?{"switch",i} -- (never printed)
        end switch
        ?{"end switch",i}
    end for
    ?"end for"
(Naturally I don’t particularly recommend any of the three [2] constructs/statements above, but it is the way it works and deserves to be documented.)
Expected output (desktop/Phix under "without js"):
{"switch",1}
{"end switch",1}
{"end switch",2}
{"end while",3}
{"end switch",3}
{"end switch",4}
{"end while",5}
{"end switch",5}
{"case",6}
"end for"
However JavaScript only has the one keyword (break) for both, and hence simply cannot distinguish between cases 3 and 4, thus the latter construct is prohibited under with js (independently by both desktop/Phix and pwa/p2js). Likewise for case 5, which were that allowed JavaScript would enter an infinite loop, and in case 6, it would print the {"end switch",6}, along with an additional {"end switch",7} that dekstop/Phix would/does not.

While it is imperative the two lines marked [1] work as expected, it is not quite as simple as saying you cannot have a break within a loop or an exit within a switch, they are valid as long as they are sufficiently well nested, whereas the three lines marked [2] trigger compilation errors under with js, and of course also trigger transpilation errors in pwa/p2js.

Aside: The Phix compiler is known to get a little antsy with unconditional exits in a loop, in effect it tries to "optimise away" the "end while" which royally messes up the zero iterations jump. Likewise "end for" and (probably) "break". So don’t do that, and obviously that’s why I’ve used the "if i=2" etc in the above demo. Obviously an unconditional exit means that the loop will never iterate, so it deserves to be removed (if always once) or replaced with an if construct (if zero or one). Occasionally it may be annoying when the compiler forces that on you but in the long run your code will thank you for it.

Should you comment out cases 4, 5, and 6 then it should compile/transpile cleanly (the former with or without js) and output
{"switch",1}
{"end switch",1}
{"end switch",2}
{"end while",3}
{"end switch",3}
{"end switch",4}
{"end switch",5}
{"end switch",6}
{"end switch",7}
"end for"