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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
--[[
 lua_parser_loose.lua.
 Loose parsing of Lua code.  See README.
 (c) 2013 David Manura. MIT License.
--]]
 
local PARSE = {}
 
local unpack = table.unpack or unpack
local LEX = require 'lua_lexer_loose'
 
--[[
 Loose parser.
 
 lx - lexer stream of Lua tokens.
 f(event...) - callback function to send events to.
 
 Events generated:
   'Var', name, lineinfo - variable declaration that immediately comes into scope.
   'VarSelf', name, lineinfo - same as 'Var' but for implicit 'self' parameter
     in method definitions.  lineinfo is zero-width space after '('
   'VarNext', name, lineinfo - variable definition that comes into scope
     upon next statement.
   'VarInside', name, lineinfo - variable definition that comes into scope
     inside following block.  Used for control variables in 'for' statements.
   'Id', name, lineinfo - reference to variable.
   'String', name - string or table field.
   'Scope', opt - beginning of scope block.
   'EndScope', nil, lineinfo - end of scope block.
   'FunctionCall', name, lineinfo - function call (in addition to other events).
   'Function', name, lineinfo - function definition.
--]]
function PARSE.parse_scope(lx, f, level)
  local cprev = {tag='Eof'}
  
  -- stack of scopes.
  local scopes = {{}}
  for l = 2, (level or 1) do scopes[l] = {} end
  
  local function scope_begin(opt, lineinfo, nobreak)
    scopes[#scopes+1] = {}
    f('Scope', opt, lineinfo, nobreak)
  end
  local function scope_end(opt, lineinfo)
    local scope = #scopes
    if scope > 1 then table.remove(scopes) end
    local inside_local = false
    for scope = scope-1, 1, -1 do
      if scopes[scope].inside_local then inside_local = true; break end
    end
    f('EndScope', opt, lineinfo, inside_local)
  end
  
  local function parse_function_list(has_self, name, pos)
    local c = lx:next(); assert(c[1] == '(')
    f('Statement', c[1], c.lineinfo, true) -- generate Statement for function definition
    scope_begin(c[1], c.lineinfo, true)
 
    local vars = {} -- accumulate vars (if any) to send after 'Function'
    if has_self then
      local lineinfo = c.lineinfo+1 -- zero size
      table.insert(vars, {'VarSelf', 'self', lineinfo, true})
    end
    while true do
      local n = lx:peek()
      if not (n.tag == 'Id' or n.tag == 'Keyword' and n[1] == '...') then break end
      local c = lx:next()
      if c.tag == 'Id' then table.insert(vars, {'Var', c[1], c.lineinfo, true}) end
      -- ignore '...' in this case
      if lx:peek()[1] == ',' then lx:next() end
    end
    if lx:peek()[1] == ')' then
      lx:next()
      f('Function', name, pos or c.lineinfo, true)
    end
    for _, var in ipairs(vars) do f(unpack(var)) end
  end
  
  while true do
    local c = lx:next()
 
    -- Detect end of previous statement
    if c.tag == 'Eof' -- trigger 'Statement' at the end of file
    or c.tag == 'Keyword' and (
       c[1] == 'break' or c[1] == 'goto' or c[1] == 'do' or c[1] == 'while' or c[1] == 'else' or
       c[1] == 'repeat' or c[1] == 'if' or c[1] == 'for' or c[1] == 'function' and lx:peek().tag == 'Id' or
       c[1] == 'local' or c[1] == ';' or c[1] == 'until' or c[1] == 'return' or c[1] == 'end')
    or c.tag == 'Id' and
           (cprev.tag == 'Id' or
            cprev.tag == 'Keyword' and
               (cprev[1] == ']' or cprev[1] == ')' or cprev[1] == '}' or
                cprev[1] == '...' or cprev[1] == 'end' or
                cprev[1] == 'true' or cprev[1] == 'false' or
                cprev[1] == 'nil') or
            cprev.tag == 'Number' or cprev.tag == 'String')
    then
      if scopes[#scopes].inside_until then scope_end(nil, c.lineinfo) end
      local scope = #scopes
      if not scopes[scope].inside_table then scopes[scope].inside_local = nil end
      f('Statement', c[1], c.lineinfo,
        scopes[scope].inside_local or c[1] == 'local' or c[1] == 'function' or c[1] == 'end')
    end
 
    if c.tag == 'Eof' then break end
    
    -- Process token(s)
    if c.tag == 'Keyword' then
    
      if c[1] == 'local' and lx:peek().tag == 'Keyword' and lx:peek()[1] == 'function' then
        -- local function
        local c = lx:next(); assert(c[1] == 'function')
        if lx:peek().tag == 'Id' then
          c = lx:next()
          f('Var', c[1], c.lineinfo, true)
          if lx:peek()[1] == '(' then parse_function_list(nil, c[1], c.lineinfo) end
        end
      elseif c[1] == 'function' then
        if lx:peek()[1] == '(' then -- inline function
          parse_function_list()
        elseif lx:peek().tag == 'Id' then -- function definition statement
          c = lx:next(); assert(c.tag == 'Id')
          local name = c[1]
          local pos = c.lineinfo
          f('Id', name, pos, true)
          local has_self
          while lx:peek()[1] ~= '(' and lx:peek().tag ~= 'Eof' do
            c = lx:next()
            name = name .. c[1]
            if c.tag == 'Id' then
              f('String', c[1], c.lineinfo, true)
            elseif c.tag == 'Keyword' and c[1] == ':' then
              has_self = true
            end
          end
          if lx:peek()[1] == '(' then parse_function_list(has_self, name, pos) end
        end
      elseif c[1] == 'local' and lx:peek().tag == 'Id' then
        scopes[#scopes].inside_local = true
        c = lx:next()
        f('VarNext', c[1], c.lineinfo, true)
        while lx:peek().tag == 'Keyword' and lx:peek()[1] == ',' do
          c = lx:next(); if lx:peek().tag ~= 'Id' then break end
          c = lx:next()
          f('VarNext', c[1], c.lineinfo, true)
        end
      elseif c[1] == 'for' and lx:peek().tag == 'Id' then
        c = lx:next()
        f('VarInside', c[1], c.lineinfo, true)
        while lx:peek().tag == 'Keyword' and lx:peek()[1] == ',' do
          c = lx:next(); if lx:peek().tag ~= 'Id' then break end
          c = lx:next()
          f('VarInside', c[1], c.lineinfo, true)
        end
      elseif c[1] == 'goto' and lx:peek().tag == 'Id' then
        lx:next()
      elseif c[1] == 'do' then
        scope_begin('do', c.lineinfo)
        -- note: do/while/for statement scopes all begin at 'do'.
      elseif c[1] == 'repeat' or c[1] == 'then' then
        scope_begin(c[1], c.lineinfo)
      elseif c[1] == 'end' or c[1] == 'elseif' then
        scope_end(c[1], c.lineinfo)
      elseif c[1] == 'else' then
        scope_end(c[1], c.lineinfo)
        scope_begin(c[1], c.lineinfo)
      elseif c[1] == 'until' then
        scopes[#scopes].inside_until = true
      elseif c[1] == '{' then
        scopes[#scopes].inside_table = (scopes[#scopes].inside_table or 0) + 1
      elseif c[1] == '}' then
        local newval = (scopes[#scopes].inside_table or 0) - 1
        newval = newval >= 1 and newval or nil
        scopes[#scopes].inside_table = newval
      end
    elseif c.tag == 'Id' then
      local scope = #scopes
      local inside_local = scopes[scope].inside_local ~= nil
      local inside_table = scopes[scope].inside_table
      local cnext = lx:peek()
      if cnext.tag == 'Keyword' and (cnext[1] == '(' or cnext[1] == '{')
      or cnext.tag == 'String' then
        f('FunctionCall', c[1], c.lineinfo, inside_local)
      end
      -- either this is inside a table or it continues from a comma,
      -- which may be a field assignment, so assume it's in a table
      if (inside_table or cprev[1] == ',') and cnext.tag == 'Keyword' and cnext[1] == '=' then
        -- table field; table fields are tricky to handle during incremental
        -- processing as "a = 1" may be either an assignment (in which case
        -- 'a' is Id) or a field initialization (in which case it's a String).
        -- Since it's not possible to decide between two cases in isolation,
        -- this is not a good place to insert a break; instead, the break is
        -- inserted at the location of the previous keyword, which allows
        -- to properly handle those cases. The desired location of
        -- the restart point is returned as the `nobreak` value.
        f('String', c[1], c.lineinfo,
          inside_local or cprev and cprev.tag == 'Keyword' and cprev.lineinfo)
      elseif cprev.tag == 'Keyword' and (cprev[1] == ':' or cprev[1] == '.') then
        f('String', c[1], c.lineinfo, true)
      else
        f('Id', c[1], c.lineinfo, true)
        -- this looks like the left side of (multi-variable) assignment
        -- unless it's a part of `= var, field = value`, so skip if inside a table;
        -- also take into account possible field assignment: `a.b, c = value`.
        -- this still doesn't handle indexing with square brackets: `a[b], c = value`.
        if not inside_table and not (cprev and cprev.tag == 'Keyword' and cprev[1] == '=') then
          local cpeek = lx:peek()
          while cpeek and cpeek.tag == 'Keyword' and (cpeek[1] == ',' or cpeek[1] == '.') do
            local c = lx:next() -- skip the keyword
            if cpeek[1] == ',' and lx:peek().tag ~= 'Id' then break end
 
            c = lx:next()
            f(cpeek[1] == ',' and 'Id' or 'String', c[1], c.lineinfo, true)
 
            cpeek = lx:peek()
          end
          if not cpeek then break end
        end
      end
    end
    
    if c.tag ~= 'Comment' then cprev = c end
  end
end
 
--[[
  This is similar to parse_scope but determines if variables are local or global.
 
  lx - lexer stream of Lua tokens.
  f(event...) - callback function to send events to.
  
  Events generated:
    'Id', name, lineinfo, 'local'|'global'
     (plus all events in parse_scope)
--]]
function PARSE.parse_scope_resolve(lx, f, vars)
  local NEXT = {}   -- unique key
  local INSIDE = {} -- unique key
  local function newscope(vars, opt, lineinfo)
    local newvars = opt=='do' and vars[INSIDE] or {}
    if newvars == vars[INSIDE] then vars[INSIDE] = false end
    newvars[INSIDE]=false
    newvars[NEXT]=false
    local level = (vars[0] or 0) + 1
    newvars[0] = level -- keep the current level
    newvars[-1] = lineinfo -- keep the start of the scope
    newvars[level] = newvars -- reference the current vars table
    return setmetatable(newvars, {__index=vars})
  end
  
  vars = vars or newscope({[0] = 0}, nil, 1)
  vars[NEXT] = false -- vars that come into scope upon next statement
  vars[INSIDE] = false -- vars that come into scope upon entering block
  PARSE.parse_scope(lx, function(op, name, lineinfo, nobreak)
    -- in some (rare) cases VarNext can follow Statement event (which copies
    -- vars[NEXT]). This may cause vars[0] to be `nil`, so default to 1.
    local var = op:find("^Var") and
      {fpos = lineinfo, at = (vars[0] or 1) + (op == 'VarInside' and 1 or 0),
       masked = vars[name], self = (op == 'VarSelf') or nil } or nil
    if op == 'Var' or op == 'VarSelf' then
      vars[name] = var
    elseif op == 'VarNext' then
      vars[NEXT] = vars[NEXT] or {}
      vars[NEXT][name] = var
    elseif op == 'VarInside' then
      vars[INSIDE] = vars[INSIDE] or {}
      vars[INSIDE][name] = var
    elseif op == 'Scope' then
      vars = newscope(vars, name, lineinfo)
    elseif op == 'EndScope' then
      local mt = getmetatable(vars)
      if mt ~= nil then vars = mt.__index end
    elseif op == 'Id'
    or op == 'String' or op == 'FunctionCall' or op == 'Function' then
      -- Just make callback
    elseif op == 'Statement' then -- beginning of statement
      -- Apply vars that come into scope upon beginning of statement.
      if vars[NEXT] then
        for k,v in pairs(vars[NEXT]) do
          vars[k] = v; vars[NEXT][k] = nil
        end
      end
    else
      assert(false)
    end
    f(op, name, lineinfo, vars, nobreak)
  end, vars[0])
end
 
function PARSE.extract_vars(code, f)
  local lx = LEX.lexc(code)
  
  local char0 = 1  -- next char offset to write
  local function gen(char1, nextchar0)
    char0 = nextchar0
  end
  
  PARSE.parse_scope_resolve(lx, function(op, name, lineinfo, other)
    if op == 'Id' then
      f('Id', name, other, lineinfo)
    elseif op == 'Var' or op == 'VarNext' or op == 'VarInside' then
      gen(lineinfo, lineinfo+#name)
      f('Var', name, "local", lineinfo)
    end  -- ignore 'VarSelf' and others
  end)
  gen(#code+1, nil)
end
 
--[[
  Converts 5.2 code to 5.1 style code with explicit _ENV variables.
  Example: "function f(_ENV, x) print(x, y)" -->
            "function _ENV.f(_ENV, x) _ENV.print(x, _ENV.y) end"
 
  code - string of Lua code.  Assumed to be valid Lua (FIX: 5.1 or 5.2?)
  f(s) - call back function to send chunks of Lua code output to.  Example: io.stdout.
--]]
function PARSE.replace_env(code, f)
  if not f then return PARSE.accumulate(PARSE.replace_env, code) end
  PARSE.extract_vars(code, function(op, name, other)
    if op == 'Id' then
      f(other == 'global' and '_ENV.' .. name or name)
    elseif op == 'Var' or op == 'Other' then
      f(name)
    end
  end)
end
 
-- helper function.  Can be passed as argument `f` to functions
-- like `replace_env` above to accumulate fragments into a single string.
function PARSE.accumulator()
  local ts = {}
  local mt = {}
  mt.__index = mt
  function mt:__call(s) ts[#ts+1] = s end
  function mt:result() return table.concat(ts) end
  return setmetatable({}, mt)
end
 
-- helper function
function PARSE.accumulate(g, code)
  local accum = PARSE.accumulator()
  g(code, accum)
  return accum:result()
end
 
return PARSE