File: switch-macro.lua

package info (click to toggle)
lua-leg 0.1.2-3
  • links: PTS
  • area: main
  • in suites: lenny
  • size: 224 kB
  • ctags: 144
  • sloc: makefile: 40; sh: 17
file content (187 lines) | stat: -rwxr-xr-x 6,404 bytes parent folder | download | duplicates (7)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
-------------------------------------------------------------------------------
-- 
-- A preprocessor which transforms a switch construct (called 'match' here) 
-- into a sequence of if-elseif blocks. There is quite a discussion going on
-- (again...) about adding switch statements in Lua 
-- (http://lua-users.org/lists/lua-l/2007-11/msg00099.html), so I decided to 
-- use it as a demonstration of Leg's capabilities.
--
-- Author: Humberto Anjos
-- 
-- $Id: switch-macro.lua,v 1.2 2007/11/19 13:34:47 hanjos Exp $
-- 
-------------------------------------------------------------------------------

-- imports
local lpeg = require 'lpeg'

local parser = require 'leg.parser'

-- aliasing
local P, V, C, Ct, Cs = lpeg.P, lpeg.V, lpeg.C, lpeg.Ct, lpeg.Cs

-- command-line arguments
local args = { ... }

-- the code to parse
subject = args[1] or [=[
  match assert(io.open(file, '*a'))
    -- no cases here, nothing is generated
  end
  
  local args = { ... }
  
  -- before match
  match #args
    -- before the first case
    when 0 do print 'No arguments!'
    -- before the second case
    when 1 do -- after case checking
      print('only one argument: ', args[1])
    when 2 do
      print('two arguments: ', args[1], args[2])
      
      local a = tonumber(args[1]) + tonumber(args[2])
      io.write(a) io.flush()
    else -- before else block
      for _, v in ipairs(args) do
        print(v)
      end
    -- after else
  end
  -- after match
  
  function eval(node, ...)
    match node.tag
      when 'Const' do 
        return node.value
      when (node.left and node.right) and 'BinOp' do 
        -- this comment won't appear in the generated code
        local op, left, right = node.op, node.left, node.right
        -- but this one will
        return op(left, right)
        -- and this one too
      when node.operand and 'UnOp' do
        local op, operand = node.op, node.operand
        
        return op(operand)
      else
        match isList(node) -- internal match statement
          when true do visit(node)
          when false do error 'Invalid node!'
        end
    end
  end
]=]

-- After reading several proposals on the Lua discussion list, I decided to 
-- implement this one:
-- 
-- match <exp>
--   when <case 1> do <block 1>
--   when <case 2> do <block 2>
--   ...
--   when <case n> do <block n>
--   else <block else>
-- end
-- 
-- where <case 1>, <case 2>, ... <case n> are Lua expressions.
-- 
-- The construct above will be converted into the following code:
--
-- do
--   local __temp__ = <exp>
--   if __temp__ == (<case 1>) then <block 1>
--   elseif __temp__ == (<case 2>) then <block 2>
--   ...
--   elseif __temp__ == (<case n>) then <block n>
--   else <block else> end
-- end
-- 
-- Implementation notes:
-- 
-- * Technically, the local variable __temp__ should receive a name provably 
--   unique in the program. But, for this example, naming it __temp__ and 
--   restricting its scope will do the trick.
-- 
-- * The local declaration will always be generated, even if there are no 
--   clauses to match. This is done because the expression in the local 
--   declaration might have side effects, which affect program semantics even
--   if no match is made.
-- 
-- * The default case, if present, must be the last clause.
-- 
-- * If there's only the default case, the local declaration and the default 
--   case's block will be generated, without an enclosing if statement.
-- 
-- * If there are no cases and no default case, only the local declaration will
--   be generated. 
-- 
-- * Some comments are captured, some are not. The comments captured are those
--   which are in the middle or at the end of a <block> statement, and are 
--   captured along with the block. The other ones are matched as part of the 
--   spacing, and consequently not captured.
-- 
-- * This is an obvious one, but: since the result is a series of if-elseif 
--   blocks, there is no fallthrough.
-- 
-- * A reasonable improvement would be allowing a case clause to have several 
--   possible matches, generating something like 
--   if __temp__ == (<case 1>) or __temp__ == (<case 2>) then <block n> ...
-- 
--   This is left as an exercise to the reader *shameless cop-out*.

-- spacing
local S = V'IGNORED' -- parser.rules.IGNORED or scanner.IGNORED could be used

-- epsilon rule
local EPSILON = V'EPSILON' / function () end

-- new matching rule. Notice that the Block rule has no captures.
local Match = (P'match' *S* C(V'Exp') *S*
              Ct((P'when' *S* C(V'Exp') *S* P'do' *S* V'Block')^0) *S*
              ((P'else' *S* V'Block') + EPSILON) *S* P'end')
              / function (exp, cases, default)
                if #cases == 0 then -- no case clauses
                  if default then -- return the local declaration and the block
                    return 'do local __temp__ = ('..exp..') '..default..' end'
                  else -- generate just the local declaration
                    return 'do local __temp__ = ('..exp..') end'
                  end
                else -- there's at least one clause
                  local str = 'do local __temp__ = ('..exp..') '
                  
                  -- generating a new if or elseif block
                  for i = 1, #cases - 3, 2 do
                    str = str..'if __temp__ == ('..cases[i]..') then '
                      ..cases[i + 1]..' else'
                  end
                  
                  -- the last case clause
                  str = str..'if __temp__ == ('..cases[#cases - 1]..') then '
                    ..cases[#cases]
                    
                  if default then -- generate the else block
                    str = str..' else '..default..' end'
                  else -- no else, just finish it
                    str = str..' end' -- end if-elseif chain
                  end
                  
                  return str..' end' -- end do
                end
              end

-- creating the LPeg pattern
local oldStat, oldBlock = parser.rules.Stat, parser.rules.Block

local MATCH = P( parser.apply { 
  -- adding Match to the list of valid Statements
  Stat = oldStat + Match, 
  
  -- the Block rule needs to be updated as well, in order to make the 
  -- necessary substitutions to inner Match statements
  Block = Cs(oldBlock)
} )

print('subject:', '\n'..subject)
print('result:', '\n'..MATCH:match(subject))