minify/optparser.lua

1
--[[--------------------------------------------------------------------
2
 
3
  optparser.lua: does parser-based optimizations
4
  This file is part of LuaSrcDiet.
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
-- * For more parser-based optimization ideas, see the TODO items or
17
--   look at technotes.txt.
18
-- * The processing load is quite significant, but since this is an
19
--   off-line text processor, I believe we can wait a few seconds.
20
-- * TODO: might process "local a,a,a" wrongly... need tests!
21
-- * TODO: remove position handling if overlapped locals (rem < 0)
22
--   needs more study, to check behaviour
23
-- * TODO: there are probably better ways to do allocation, e.g. by
24
--   choosing better methods to sort and pick locals...
25
-- * TODO: we don't need 53*63 two-letter identifiers; we can make
26
--   do with significantly less depending on how many that are really
27
--   needed and improve entropy; e.g. 13 needed -> choose 4*4 instead
28
----------------------------------------------------------------------]]
29
 
30
local base = _G
31
local string = require "string"
32
local table = require "table"
33
 
34
----------------------------------------------------------------------
35
-- Letter frequencies for reducing symbol entropy (fixed version)
36
-- * Might help a wee bit when the output file is compressed
37
-- * See Wikipedia: http://en.wikipedia.org/wiki/Letter_frequencies
38
-- * We use letter frequencies according to a Linotype keyboard, plus
39
--   the underscore, and both lower case and upper case letters.
40
-- * The arrangement below (LC, underscore, %d, UC) is arbitrary.
41
-- * This is certainly not optimal, but is quick-and-dirty and the
42
--   process has no significant overhead
43
----------------------------------------------------------------------
44
 
45
local LETTERS = "etaoinshrdlucmfwypvbgkqjxz_ETAOINSHRDLUCMFWYPVBGKQJXZ"
46
local ALPHANUM = "etaoinshrdlucmfwypvbgkqjxz_0123456789ETAOINSHRDLUCMFWYPVBGKQJXZ"
47
 
48
-- names or identifiers that must be skipped
49
-- * the first two lines are for keywords
50
local SKIP_NAME = {}
51
for v in string.gmatch([[
52
and break do else elseif end false for function if in
53
local nil not or repeat return then true until while
54
self _ENV]], "%S+") do
55
  SKIP_NAME[v] = true
56
end
57
 
58
------------------------------------------------------------------------
59
-- variables and data structures
60
------------------------------------------------------------------------
61
 
62
local toklist, seminfolist,             -- token lists
63
      globalinfo, localinfo,            -- variable information tables
64
      globaluniq, localuniq,            -- unique name tables
65
      var_new,                          -- index of new variable names
66
      varlist                           -- list of output variables
67
 
68
----------------------------------------------------------------------
69
-- preprocess information table to get lists of unique names
70
----------------------------------------------------------------------
71
 
72
local function preprocess(infotable)
73
  local uniqtable = {}
74
  for i = 1, #infotable do              -- enumerate info table
75
    local obj = infotable[i]
76
    local name = obj.name
77
    --------------------------------------------------------------------
78
    if not uniqtable[name] then         -- not found, start an entry
79
      uniqtable[name] = {
80
        decl = 0, token = 0, size = 0,
81
      }
82
    end
83
    --------------------------------------------------------------------
84
    local uniq = uniqtable[name]        -- count declarations, tokens, size
85
    uniq.decl = uniq.decl + 1
86
    local xref = obj.xref
87
    local xcount = #xref
88
    uniq.token = uniq.token + xcount
89
    uniq.size = uniq.size + xcount * #name
90
    --------------------------------------------------------------------
91
    if obj.decl then            -- if local table, create first,last pairs
92
      obj.id = i
93
      obj.xcount = xcount
94
      if xcount > 1 then        -- if ==1, means local never accessed
95
        obj.first = xref[2]
96
        obj.last = xref[xcount]
97
      end
98
    --------------------------------------------------------------------
99
    else                        -- if global table, add a back ref
100
      uniq.id = i
101
    end
102
    --------------------------------------------------------------------
103
  end--for
104
  return uniqtable
105
end
106
 
107
----------------------------------------------------------------------
108
-- calculate actual symbol frequencies, in order to reduce entropy
109
-- * this may help further reduce the size of compressed sources
110
-- * note that since parsing optimizations is put before lexing
111
--   optimizations, the frequency table is not exact!
112
-- * yes, this will miss --keep block comments too...
113
----------------------------------------------------------------------
114
 
115
local function recalc_for_entropy(option)
116
  local byte = string.byte
117
  local char = string.char
118
  -- table of token classes to accept in calculating symbol frequency
119
  local ACCEPT = {
120
    TK_KEYWORD = true, TK_NAME = true, TK_NUMBER = true,
121
    TK_STRING = true, TK_LSTRING = true,
122
  }
123
  if not option["opt-comments"] then
124
    ACCEPT.TK_COMMENT = true
125
    ACCEPT.TK_LCOMMENT = true
126
  end
127
  --------------------------------------------------------------------
128
  -- create a new table and remove any original locals by filtering
129
  --------------------------------------------------------------------
130
  local filtered = {}
131
  for i = 1, #toklist do
132
    filtered[i] = seminfolist[i]
133
  end
134
  for i = 1, #localinfo do              -- enumerate local info table
135
    local obj = localinfo[i]
136
    local xref = obj.xref
137
    for j = 1, obj.xcount do
138
      local p = xref[j]
139
      filtered[p] = ""                  -- remove locals
140
    end
141
  end
142
  --------------------------------------------------------------------
143
  local freq = {}                       -- reset symbol frequency table
144
  for i = 0, 255 do freq[i] = 0 end
145
  for i = 1, #toklist do                -- gather symbol frequency
146
    local tok, info = toklist[i], filtered[i]
147
    if ACCEPT[tok] then
148
      for j = 1, #info do
149
        local c = byte(info, j)
150
        freq[c] = freq[c] + 1
151
      end
152
    end--if
153
  end--for
154
  --------------------------------------------------------------------
155
  -- function to re-sort symbols according to actual frequencies
156
  --------------------------------------------------------------------
157
  local function resort(symbols)
158
    local symlist = {}
159
    for i = 1, #symbols do              -- prepare table to sort
160
      local c = byte(symbols, i)
161
      symlist[i] = { c = c, freq = freq[c], }
162
    end
163
    table.sort(symlist,                 -- sort selected symbols
164
      function(v1, v2)
165
        return v1.freq > v2.freq
166
      end
167
    )
168
    local charlist = {}                 -- reconstitute the string
169
    for i = 1, #symlist do
170
      charlist[i] = char(symlist[i].c)
171
    end
172
    return table.concat(charlist)
173
  end
174
  --------------------------------------------------------------------
175
  LETTERS = resort(LETTERS)             -- change letter arrangement
176
  ALPHANUM = resort(ALPHANUM)
177
end
178
 
179
----------------------------------------------------------------------
180
-- returns a string containing a new local variable name to use, and
181
-- a flag indicating whether it collides with a global variable
182
-- * trapping keywords and other names like 'self' is done elsewhere
183
----------------------------------------------------------------------
184
 
185
local function new_var_name()
186
  local var
187
  local cletters, calphanum = #LETTERS, #ALPHANUM
188
  local v = var_new
189
  if v < cletters then                  -- single char
190
    v = v + 1
191
    var = string.sub(LETTERS, v, v)
192
  else                                  -- longer names
193
    local range, sz = cletters, 1       -- calculate # chars fit
194
    repeat
195
      v = v - range
196
      range = range * calphanum
197
      sz = sz + 1
198
    until range > v
199
    local n = v % cletters              -- left side cycles faster
200
    v = (v - n) / cletters              -- do first char first
201
    n = n + 1
202
    var = string.sub(LETTERS, n, n)
203
    while sz > 1 do
204
      local m = v % calphanum
205
      v = (v - m) / calphanum
206
      m = m + 1
207
      var = var..string.sub(ALPHANUM, m, m)
208
      sz = sz - 1
209
    end
210
  end
211
  var_new = var_new + 1
212
  return var, globaluniq[var] ~= nil
213
end
214
 
215
----------------------------------------------------------------------
216
-- main entry point
217
-- * does only local variable optimization for now
218
----------------------------------------------------------------------
219
 
220
function optimize(option, _toklist, _seminfolist, _globalinfo, _localinfo)
221
  -- set tables
222
  toklist, seminfolist, globalinfo, localinfo
223
    = _toklist, _seminfolist, _globalinfo, _localinfo
224
  var_new = 0                           -- reset variable name allocator
225
  varlist = {}
226
  ------------------------------------------------------------------
227
  -- preprocess global/local tables, handle entropy reduction
228
  ------------------------------------------------------------------
229
  globaluniq = preprocess(globalinfo)
230
  localuniq = preprocess(localinfo)
231
  if option["opt-entropy"] then         -- for entropy improvement
232
    recalc_for_entropy(option)
233
  end
234
  ------------------------------------------------------------------
235
  -- build initial declared object table, then sort according to
236
  -- token count, this might help assign more tokens to more common
237
  -- variable names such as 'e' thus possibly reducing entropy
238
  -- * an object knows its localinfo index via its 'id' field
239
  -- * special handling for "self" special local (parameter) here
240
  ------------------------------------------------------------------
241
  local object = {}
242
  for i = 1, #localinfo do
243
    object[i] = localinfo[i]
244
  end
245
  table.sort(object,                    -- sort largest first
246
    function(v1, v2)
247
      return v1.xcount > v2.xcount
248
    end
249
  )
250
  ------------------------------------------------------------------
251
  -- the special "self" function parameters must be preserved
252
  -- * the allocator below will never use "self", so it is safe to
253
  --   keep those implicit declarations as-is
254
  ------------------------------------------------------------------
255
  local temp, j, gotself = {}, 1, false
256
  for i = 1, #object do
257
    local obj = object[i]
258
    if not obj.preserve then
259
      temp[j] = obj
260
      j = j + 1
261
    elseif obj.name == "self" then
262
      gotself = true
263
    end
264
  end
265
  object = temp
266
  ------------------------------------------------------------------
267
  -- a simple first-come first-served heuristic name allocator,
268
  -- note that this is in no way optimal...
269
  -- * each object is a local variable declaration plus existence
270
  -- * the aim is to assign short names to as many tokens as possible,
271
  --   so the following tries to maximize name reuse
272
  -- * note that we preserve sort order
273
  ------------------------------------------------------------------
274
  local nobject = #object
275
  while nobject > 0 do
276
    local varname, gcollide
277
    repeat
278
      varname, gcollide = new_var_name()  -- collect a variable name
279
    until not SKIP_NAME[varname]          -- skip all special names
280
    varlist[#varlist + 1] = varname       -- keep a list
281
    local oleft = nobject
282
    ------------------------------------------------------------------
283
    -- if variable name collides with an existing global, the name
284
    -- cannot be used by a local when the name is accessed as a global
285
    -- during which the local is alive (between 'act' to 'rem'), so
286
    -- we drop objects that collides with the corresponding global
287
    ------------------------------------------------------------------
288
    if gcollide then
289
      -- find the xref table of the global
290
      local gref = globalinfo[globaluniq[varname].id].xref
291
      local ngref = #gref
292
      -- enumerate for all current objects; all are valid at this point
293
      for i = 1, nobject do
294
        local obj = object[i]
295
        local act, rem = obj.act, obj.rem  -- 'live' range of local
296
        -- if rem < 0, it is a -id to a local that had the same name
297
        -- so follow rem to extend it; does this make sense?
298
        while rem < 0 do
299
          rem = localinfo[-rem].rem
300
        end
301
        local drop
302
        for j = 1, ngref do
303
          local p = gref[j]
304
          if p >= act and p <= rem then drop = true end  -- in range?
305
        end
306
        if drop then
307
          obj.skip = true
308
          oleft = oleft - 1
309
        end
310
      end--for
311
    end--if gcollide
312
    ------------------------------------------------------------------
313
    -- now the first unassigned local (since it's sorted) will be the
314
    -- one with the most tokens to rename, so we set this one and then
315
    -- eliminate all others that collides, then any locals that left
316
    -- can then reuse the same variable name; this is repeated until
317
    -- all local declaration that can use this name is assigned
318
    -- * the criteria for local-local reuse/collision is:
319
    --   A is the local with a name already assigned
320
    --   B is the unassigned local under consideration
321
    --   => anytime A is accessed, it cannot be when B is 'live'
322
    --   => to speed up things, we have first/last accesses noted
323
    ------------------------------------------------------------------
324
    while oleft > 0 do
325
      local i = 1
326
      while object[i].skip do  -- scan for first object
327
        i = i + 1
328
      end
329
      ------------------------------------------------------------------
330
      -- first object is free for assignment of the variable name
331
      -- [first,last] gives the access range for collision checking
332
      ------------------------------------------------------------------
333
      oleft = oleft - 1
334
      local obja = object[i]
335
      i = i + 1
336
      obja.newname = varname
337
      obja.skip = true
338
      obja.done = true
339
      local first, last = obja.first, obja.last
340
      local xref = obja.xref
341
      ------------------------------------------------------------------
342
      -- then, scan all the rest and drop those colliding
343
      -- if A was never accessed then it'll never collide with anything
344
      -- otherwise trivial skip if:
345
      -- * B was activated after A's last access (last < act)
346
      -- * B was removed before A's first access (first > rem)
347
      -- if not, see detailed skip below...
348
      ------------------------------------------------------------------
349
      if first and oleft > 0 then  -- must have at least 1 access
350
        local scanleft = oleft
351
        while scanleft > 0 do
352
          while object[i].skip do  -- next valid object
353
            i = i + 1
354
          end
355
          scanleft = scanleft - 1
356
          local objb = object[i]
357
          i = i + 1
358
          local act, rem = objb.act, objb.rem  -- live range of B
359
          -- if rem < 0, extend range of rem thru' following local
360
          while rem < 0 do
361
            rem = localinfo[-rem].rem
362
          end
363
          --------------------------------------------------------
364
          if not(last < act or first > rem) then  -- possible collision
365
            --------------------------------------------------------
366
            -- B is activated later than A or at the same statement,
367
            -- this means for no collision, A cannot be accessed when B
368
            -- is alive, since B overrides A (or is a peer)
369
            --------------------------------------------------------
370
            if act >= obja.act then
371
              for j = 1, obja.xcount do  -- ... then check every access
372
                local p = xref[j]
373
                if p >= act and p <= rem then  -- A accessed when B live!
374
                  oleft = oleft - 1
375
                  objb.skip = true
376
                  break
377
                end
378
              end--for
379
            --------------------------------------------------------
380
            -- A is activated later than B, this means for no collision,
381
            -- A's access is okay since it overrides B, but B's last
382
            -- access need to be earlier than A's activation time
383
            --------------------------------------------------------
384
            else
385
              if objb.last and objb.last >= obja.act then
386
                oleft = oleft - 1
387
                objb.skip = true
388
              end
389
            end
390
          end
391
          --------------------------------------------------------
392
          if oleft == 0 then break end
393
        end
394
      end--if first
395
      ------------------------------------------------------------------
396
    end--while
397
    ------------------------------------------------------------------
398
    -- after assigning all possible locals to one variable name, the
399
    -- unassigned locals/objects have the skip field reset and the table
400
    -- is compacted, to hopefully reduce iteration time
401
    ------------------------------------------------------------------
402
    local temp, j = {}, 1
403
    for i = 1, nobject do
404
      local obj = object[i]
405
      if not obj.done then
406
        obj.skip = false
407
        temp[j] = obj
408
        j = j + 1
409
      end
410
    end
411
    object = temp  -- new compacted object table
412
    nobject = #object  -- objects left to process
413
    ------------------------------------------------------------------
414
  end--while
415
  ------------------------------------------------------------------
416
  -- after assigning all locals with new variable names, we can
417
  -- patch in the new names, and reprocess to get 'after' stats
418
  ------------------------------------------------------------------
419
  for i = 1, #localinfo do  -- enumerate all locals
420
    local obj = localinfo[i]
421
    local xref = obj.xref
422
    if obj.newname then                 -- if got new name, patch it in
423
      for j = 1, obj.xcount do
424
        local p = xref[j]               -- xrefs indexes the token list
425
        seminfolist[p] = obj.newname
426
      end
427
      obj.name, obj.oldname             -- adjust names
428
        = obj.newname, obj.name
429
    else
430
      obj.oldname = obj.name            -- for cases like 'self'
431
    end
432
  end
433
  ------------------------------------------------------------------
434
  -- deal with statistics output
435
  ------------------------------------------------------------------
436
  if gotself then  -- add 'self' to end of list
437
    varlist[#varlist + 1] = "self"
438
  end
439
  local afteruniq = preprocess(localinfo)
440
  ------------------------------------------------------------------
441
end
442
 
443
return {optimize = optimize}