optparser.lua 23 KB

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