minify/llex.lua

1
--[[--------------------------------------------------------------------
2
 
3
  llex.lua: Lua 5.1 lexical analyzer in Lua
4
  This file is part of LuaSrcDiet, based on Yueliang material.
5
 
6
  Copyright (c) 2008 Kein-Hong Man <khman@users.sf.net>
7
  The COPYRIGHT file describes the conditions
8
  under which this software may be distributed.
9
 
10
  See the ChangeLog for more information.
11
 
12
----------------------------------------------------------------------]]
13
 
14
--[[--------------------------------------------------------------------
15
-- NOTES:
16
-- * This is a version of the native 5.1.x lexer from Yueliang 0.4.0,
17
--   with significant modifications to handle LuaSrcDiet's needs:
18
--   (1) llex.error is an optional error function handler
19
--   (2) seminfo for strings include their delimiters and no
20
--       translation operations are performed on them
21
-- * ADDED shbang handling has been added to support executable scripts
22
-- * NO localized decimal point replacement magic
23
-- * NO limit to number of lines
24
-- * NO support for compatible long strings (LUA_COMPAT_LSTR)
25
-- * Please read technotes.txt for more technical details.
26
----------------------------------------------------------------------]]
27
 
28
local base = _G
29
local string = require "string"
30
--module "llex"
31
 
32
local find = string.find
33
local match = string.match
34
local sub = string.sub
35
 
36
local  z = ''                        -- source
37
local  sourceid = ''          -- name of source
38
local  I = 1                         -- lexer's position in source
39
local  ln = 1                        -- line number
40
local  tok = {}                      -- lexed token list*
41
local  seminfo = {}                  -- lexed semantic information list*
42
local  tokln = {}                    -- line numbers for messages*
43
local buff = ''
44
-- NOTE: see init() for module variables (externally visible):
45
--       tok, seminfo, tokln
46
 
47
----------------------------------------------------------------------
48
-- initialize keyword list, variables
49
----------------------------------------------------------------------
50
 
51
local kw = {}
52
for v in string.gmatch([[
53
and break do else elseif end false for function if in
54
local nil not or repeat return then true until while]], "%S+") do
55
  kw[v] = true
56
end
57
 
58
 
59
----------------------------------------------------------------------
60
-- add information to token listing
61
----------------------------------------------------------------------
62
 
63
local function addtoken(token, info)
64
  local i = #tok + 1
65
  tok[i] = token
66
  seminfo[i] = info
67
  tokln[i] = ln
68
end
69
 
70
----------------------------------------------------------------------
71
-- handles line number incrementation and end-of-line characters
72
----------------------------------------------------------------------
73
 
74
local function inclinenumber(i, is_tok)
75
  local sub = sub
76
  local old = sub(z, i, i)
77
  i = i + 1  -- skip '\n' or '\r'
78
  local c = sub(z, i, i)
79
  if (c == "\n" or c == "\r") and (c ~= old) then
80
    i = i + 1  -- skip '\n\r' or '\r\n'
81
    old = old..c
82
  end
83
  if is_tok then addtoken("TK_EOL", old) end
84
  ln = ln + 1
85
  I = i
86
  return i
87
end
88
 
89
----------------------------------------------------------------------
90
-- initialize lexer for given source _z and source name _sourceid
91
----------------------------------------------------------------------
92
 
93
function init(_z, _sourceid)
94
  z = _z                        -- source
95
  sourceid = _sourceid         -- name of source
96
  --------------------------------------------------------------------
97
  -- initial processing (shbang handling)
98
  --------------------------------------------------------------------
99
  local p, _, q, r = find(z, "^(#[^\r\n]*)(\r?\n?)")
100
  if p then                             -- skip first line
101
    I = I + #q
102
    addtoken("TK_COMMENT", q)
103
    if #r > 0 then inclinenumber(I, true) end
104
  end
105
end
106
 
107
----------------------------------------------------------------------
108
-- returns a chunk name or id, no truncation for long names
109
----------------------------------------------------------------------
110
 
111
function chunkid()
112
  if sourceid and match(sourceid, "^[=@]") then
113
    return sub(sourceid, 2)  -- remove first char
114
  end
115
  return "[string]"
116
end
117
 
118
----------------------------------------------------------------------
119
-- formats error message and throws error
120
-- * a simplified version, does not report what token was responsible
121
----------------------------------------------------------------------
122
 
123
function errorline(s, line)
124
  local e = error or base.error
125
  e(string.format("%s:%d: %s", chunkid(), line or ln, s))
126
end
127
local errorline = errorline
128
 
129
------------------------------------------------------------------------
130
-- count separators ("=") in a long string delimiter
131
------------------------------------------------------------------------
132
 
133
local function skip_sep(i)
134
  local sub = sub
135
  local s = sub(z, i, i)
136
  i = i + 1
137
  local count = #match(z, "=*", i)  -- note, take the length
138
  i = i + count
139
  I = i
140
  return (sub(z, i, i) == s) and count or (-count) - 1
141
end
142
 
143
----------------------------------------------------------------------
144
-- reads a long string or long comment
145
----------------------------------------------------------------------
146
 
147
local function read_long_string(is_str, sep)
148
  local i = I + 1  -- skip 2nd '['
149
  local sub = sub
150
  local c = sub(z, i, i)
151
  if c == "\r" or c == "\n" then  -- string starts with a newline?
152
    i = inclinenumber(i)  -- skip it
153
  end
154
  local j = i
155
  while true do
156
    local p, q, r = find(z, "([\r\n%]])", i) -- (long range)
157
    if not p then
158
      errorline(is_str and "unfinished long string" or
159
                "unfinished long comment")
160
    end
161
    i = p
162
    if r == "]" then                    -- delimiter test
163
      if skip_sep(i) == sep then
164
        buff = sub(z, buff, I)
165
        I = I + 1  -- skip 2nd ']'
166
        return buff
167
      end
168
      i = I
169
    else                                -- newline
170
      buff = buff.."\n"
171
      i = inclinenumber(i)
172
    end
173
  end--while
174
end
175
 
176
----------------------------------------------------------------------
177
-- reads a string
178
----------------------------------------------------------------------
179
 
180
local function read_string(del)
181
  local i = I
182
  local find = find
183
  local sub = sub
184
  while true do
185
    local p, q, r = find(z, "([\n\r\\\"\'])", i) -- (long range)
186
    if p then
187
      if r == "\n" or r == "\r" then
188
        errorline("unfinished string")
189
      end
190
      i = p
191
      if r == "\\" then                         -- handle escapes
192
        i = i + 1
193
        r = sub(z, i, i)
194
        if r == "" then break end -- (EOZ error)
195
        p = find("abfnrtv\n\r", r, 1, true)
196
        ------------------------------------------------------
197
        if p then                               -- special escapes
198
          if p > 7 then
199
            i = inclinenumber(i)
200
          else
201
            i = i + 1
202
          end
203
        ------------------------------------------------------
204
        elseif find(r, "%D") then               -- other non-digits
205
          i = i + 1
206
        ------------------------------------------------------
207
        else                                    -- \xxx sequence
208
          local p, q, s = find(z, "^(%d%d?%d?)", i)
209
          i = q + 1
210
          if s + 1 > 256 then -- UCHAR_MAX
211
            errorline("escape sequence too large")
212
          end
213
        ------------------------------------------------------
214
        end--if p
215
      else
216
        i = i + 1
217
        if r == del then                        -- ending delimiter
218
          I = i
219
          return sub(z, buff, i - 1)            -- return string
220
        end
221
      end--if r
222
    else
223
      break -- (error)
224
    end--if p
225
  end--while
226
  errorline("unfinished string")
227
end
228
 
229
------------------------------------------------------------------------
230
-- main lexer function
231
------------------------------------------------------------------------
232
 
233
function llex()
234
  local find = find
235
  local match = match
236
  while true do--outer
237
    local i = I
238
    -- inner loop allows break to be used to nicely section tests
239
    while true do--inner
240
      ----------------------------------------------------------------
241
      local p, _, r = find(z, "^([_%a][_%w]*)", i)
242
      if p then
243
        I = i + #r
244
        if kw[r] then
245
          addtoken("TK_KEYWORD", r)             -- reserved word (keyword)
246
        else
247
          addtoken("TK_NAME", r)                -- identifier
248
        end
249
        break -- (continue)
250
      end
251
      ----------------------------------------------------------------
252
      local p, _, r = find(z, "^(%.?)%d", i)
253
      if p then                                 -- numeral
254
        if r == "." then i = i + 1 end
255
        local _, q, r = find(z, "^%d*[%.%d]*([eE]?)", i)
256
        i = q + 1
257
        if #r == 1 then                         -- optional exponent
258
          if match(z, "^[%+%-]", i) then        -- optional sign
259
            i = i + 1
260
          end
261
        end
262
        local _, q = find(z, "^[_%w]*", i)
263
        I = q + 1
264
        local v = sub(z, p, q)                  -- string equivalent
265
        if not base.tonumber(v) then            -- handles hex test also
266
          errorline("malformed number")
267
        end
268
        addtoken("TK_NUMBER", v)
269
        break -- (continue)
270
      end
271
      ----------------------------------------------------------------
272
      local p, q, r, t = find(z, "^((%s)[ \t\v\f]*)", i)
273
      if p then
274
        if t == "\n" or t == "\r" then          -- newline
275
          inclinenumber(i, true)
276
        else
277
          I = q + 1                             -- whitespace
278
          addtoken("TK_SPACE", r)
279
        end
280
        break -- (continue)
281
      end
282
      ----------------------------------------------------------------
283
      local r = match(z, "^%p", i)
284
      if r then
285
        buff = i
286
        local p = find("-[\"\'.=<>~", r, 1, true)
287
        if p then
288
          -- two-level if block for punctuation/symbols
289
          --------------------------------------------------------
290
          if p <= 2 then
291
            if p == 1 then                      -- minus
292
              local c = match(z, "^%-%-(%[?)", i)
293
              if c then
294
                i = i + 2
295
                local sep = -1
296
                if c == "[" then
297
                  sep = skip_sep(i)
298
                end
299
                if sep >= 0 then                -- long comment
300
                  addtoken("TK_LCOMMENT", read_long_string(false, sep))
301
                else                            -- short comment
302
                  I = find(z, "[\n\r]", i) or (#z + 1)
303
                  addtoken("TK_COMMENT", sub(z, buff, I - 1))
304
                end
305
                break -- (continue)
306
              end
307
              -- (fall through for "-")
308
            else                                -- [ or long string
309
              local sep = skip_sep(i)
310
              if sep >= 0 then
311
                addtoken("TK_LSTRING", read_long_string(true, sep))
312
              elseif sep == -1 then
313
                addtoken("TK_OP", "[")
314
              else
315
                errorline("invalid long string delimiter")
316
              end
317
              break -- (continue)
318
            end
319
          --------------------------------------------------------
320
          elseif p <= 5 then
321
            if p < 5 then                       -- strings
322
              I = i + 1
323
              addtoken("TK_STRING", read_string(r))
324
              break -- (continue)
325
            end
326
            r = match(z, "^%.%.?%.?", i)        -- .|..|... dots
327
            -- (fall through)
328
          --------------------------------------------------------
329
          else                                  -- relational
330
            r = match(z, "^%p=?", i)
331
            -- (fall through)
332
          end
333
        end
334
        I = i + #r
335
        addtoken("TK_OP", r)  -- for other symbols, fall through
336
        break -- (continue)
337
      end
338
      ----------------------------------------------------------------
339
      local r = sub(z, i, i)
340
      if r ~= "" then
341
        I = i + 1
342
        addtoken("TK_OP", r)                    -- other single-char tokens
343
        break
344
      end
345
      addtoken("TK_EOS", "")                    -- end of stream,
346
      return                                    -- exit here
347
      ----------------------------------------------------------------
348
    end--while inner
349
  end--while outer
350
end
351
 
352
return {
353
llex = llex,
354
init = init,
355
chunkid = chunkid,
356
tok = tok,
357
tokln = tokln,
358
seminfo = seminfo
359
}