lua-users home
lua-l archive

[Date Prev][Date Next][Thread Prev][Thread Next] [Date Index] [Thread Index]


On Wed, Nov 20, 2013 at 1:20 PM, Sean Conner <sean@conman.org> wrote:
It was thus said that the Great Paige DePol once stated:
> When I first started hacking Lua I really wanted to implement a C style
> switch/case statement, back then I had no idea how to even go about such
> an endeavour! Well, I am now very happy to say that I have successfully
> implemented the switch/case statement for Lua, and have attached the patch
> to this post.
>
> This patch does NOT require any special variant of Lua. Code compiled with
> this patch WILL run on any non-patched Lua virtual machine as it only uses
> the standard flow control opcodes already present in Lua (OP_NE and
> OP_JMP).
>
> The bytecode generated by this patch is the same size as an equivalent
> if/elseif/else block (same number of if/elseif as cases in switch block).
> The switch statement creates a temp local variable (as the for loops do)
> to hold the test condition for the case tests. This is very useful when
> testing a global variable as the global is only loaded once at the start
> of the switch, unlike if/elseif which potentially would test the global at
> each step.
>
> Actually, if every case statement has a break then the resulting bytecode
> will be 1 byte larger than the same if/elseif statements, but only if both
> are using a local. If a global is involved then the switch bytecode will
> always be smaller than the if/elseif bytecode.

  With "break", it's not quite the same as an if/elseif/else block.  For
instance (first example):

        switch(x)
          case 1
            print("foo")
            break
          case 2
            print("bar")
            break
          else
            print("wtf")
        end

*is* the same as:

        if x == 1 then
          print("foo")
        elseif x == 2 then
          print("bar")
        else
          print("wtf")
        end

But this:

        switch(x)
          case 1
            print("foo")
          case 2
            print("bar")
          else
            print("wtf")
        end

is more like:

        if x == 1 then
          print("foo")
        end
        if x == 1 or x == 2 then
          print("bar")
        end
        -- ... um ... is "wtf" always printed?

> Syntax of the switch/case statement:
>
> switch <variable>      -- variable must be global or local
>   case <constant>      -- constant: nil, true, false, number or string
>     <block>            -- standard Lua block, all statements valid
>     [break]            -- optional break, if not present will fall through
>   case <constant>      -- to the block of the next case statement
>     <block>
>     [break]            -- repeat as many cases as you need
>   case <constant>      -- specifying a constant more than once will
>     <block>            -- generate a syntax error at compile time
>     [break]
>   else                 -- optional else block if no case conditions matched
>     <block>            -- if last statement in else is a break it will be
> end                    -- optimised out and will not appear in the bytecode
>
> Due to the limitations of how this needed to be implemented it is wise to
> put the most frequent case conditions at the top of the switch statement,
> as each case needs to be checked in turn until one matches. This is
> achieved by using OP_NE at the first case, if false then an OP_JMP is done
> to the next case, and if no matches then to the 'else' block or out of the
> switch block.

  Okay, if each case has a "break" statement [1] then an optimization might
be to convert the switch statement to a table.  Go, going back to my first
example, it would internally be converted to:

        _switch0000 = {
                [1] = function() print("foo") end,
                [2] = function() print("bar") end,
        }

        if _switch0000[x] == nil then
          print("wtf")
        else
          _switch0000[x]()
        end

  Actually, now that I think about it, the breakless blocks can be
implemented as:

        _switch0001 = {
                [1] = function() print("foo") _switch0001[2]() end,
                [2] = function() print("bar") _switch0001._default() end,
                _default = function() print("wtf") end,
        end

        if _switch0001[x] == nil then
          _switch0001._default()
        else
          _switch0001[x]()
        end

> For my Lunia engine I plan to implement a new VM opcode; OP_JMPTBL. This
> will essentially use the constant number as an offset into a jump table to
> simply jump to the correct case block, negating the need to scan down the
> case statements to find the right one. This can not be done with the
> standard Lua distribution, however, the patch attached to this post does
> work with vanilla Lua! :)

  Mind the gap!

        switch(x)
          case 10 ... break
          case 20 ... break
          case 100 ... break
        end

  Or even ... Mind the type!

        switch(x)
          case true ... break
          case nil  ... break
          case 10   ... break
          case "foobar" ... break
          case print ... break
        end

  -spc (I've found that I don't really miss "switch" as much as I thought I
        would ... )

[1]     That is, if you keep that syntax.  The number of times I've *not*
        used "break" in C code I can cound on the fingers of one hand and
        still have a few fingers left over.  It might be better to have a
        "fallthrough" statement.


switch/case is something I often miss in Lua. You can do the same with a table of functions but it's a lot uglier. (And probably a lot less efficient due to the overhead of creating a bunch of functions and the table to hold them, vs what translates to a few if/else statements.)

--
Sent from my Game Boy.