Commit from LuCI Translation Portal by user jow.: 155 of 156 messages translated...
[project/luci.git] / contrib / luasrcdiet / lua / LuaSrcDiet.lua
1 #!/usr/bin/env lua
2 --[[--------------------------------------------------------------------
3
4   LuaSrcDiet
5   Compresses Lua source code by removing unnecessary characters.
6   For Lua 5.1.x source code.
7
8   Copyright (c) 2008 Kein-Hong Man <khman@users.sf.net>
9   The COPYRIGHT file describes the conditions
10   under which this software may be distributed.
11
12   See the ChangeLog for more information.
13
14 ----------------------------------------------------------------------]]
15
16 --[[--------------------------------------------------------------------
17 -- NOTES:
18 -- * Remember to update version and date information below (MSG_TITLE)
19 -- * TODO: to implement pcall() to properly handle lexer etc. errors
20 -- * TODO: verify token stream or double-check binary chunk?
21 -- * TODO: need some automatic testing for a semblance of sanity
22 -- * TODO: the plugin module is highly experimental and unstable
23 ----------------------------------------------------------------------]]
24
25 -- standard libraries, functions
26 local string = string
27 local math = math
28 local table = table
29 local require = require
30 local print = print
31 local sub = string.sub
32 local gmatch = string.gmatch
33
34 -- support modules
35 local llex = require "llex"
36 local lparser = require "lparser"
37 local optlex = require "optlex"
38 local optparser = require "optparser"
39 local plugin
40
41 --[[--------------------------------------------------------------------
42 -- messages and textual data
43 ----------------------------------------------------------------------]]
44
45 local MSG_TITLE = [[
46 LuaSrcDiet: Puts your Lua 5.1 source code on a diet
47 Version 0.11.2 (20080608)  Copyright (c) 2005-2008 Kein-Hong Man
48 The COPYRIGHT file describes the conditions under which this
49 software may be distributed.
50 ]]
51
52 local MSG_USAGE = [[
53 usage: LuaSrcDiet [options] [filenames]
54
55 example:
56   >LuaSrcDiet myscript.lua -o myscript_.lua
57
58 options:
59   -v, --version       prints version information
60   -h, --help          prints usage information
61   -o <file>           specify file name to write output
62   -s <suffix>         suffix for output files (default '_')
63   --keep <msg>        keep block comment with <msg> inside
64   --plugin <module>   run <module> in plugin/ directory
65   -                   stop handling arguments
66
67   (optimization levels)
68   --none              all optimizations off (normalizes EOLs only)
69   --basic             lexer-based optimizations only
70   --maximum           maximize reduction of source
71
72   (informational)
73   --quiet             process files quietly
74   --read-only         read file and print token stats only
75   --dump-lexer        dump raw tokens from lexer to stdout
76   --dump-parser       dump variable tracking tables from parser
77   --details           extra info (strings, numbers, locals)
78
79 features (to disable, insert 'no' prefix like --noopt-comments):
80 %s
81 default settings:
82 %s]]
83
84 ------------------------------------------------------------------------
85 -- optimization options, for ease of switching on and off
86 -- * positive to enable optimization, negative (no) to disable
87 -- * these options should follow --opt-* and --noopt-* style for now
88 ------------------------------------------------------------------------
89
90 local OPTION = [[
91 --opt-comments,'remove comments and block comments'
92 --opt-whitespace,'remove whitespace excluding EOLs'
93 --opt-emptylines,'remove empty lines'
94 --opt-eols,'all above, plus remove unnecessary EOLs'
95 --opt-strings,'optimize strings and long strings'
96 --opt-numbers,'optimize numbers'
97 --opt-locals,'optimize local variable names'
98 --opt-entropy,'tries to reduce symbol entropy of locals'
99 ]]
100
101 -- preset configuration
102 local DEFAULT_CONFIG = [[
103   --opt-comments --opt-whitespace --opt-emptylines
104   --opt-numbers --opt-locals
105 ]]
106 -- override configurations: MUST explicitly enable/disable everything
107 local BASIC_CONFIG = [[
108   --opt-comments --opt-whitespace --opt-emptylines
109   --noopt-eols --noopt-strings --noopt-numbers
110   --noopt-locals
111 ]]
112 local MAXIMUM_CONFIG = [[
113   --opt-comments --opt-whitespace --opt-emptylines
114   --opt-eols --opt-strings --opt-numbers
115   --opt-locals --opt-entropy
116 ]]
117 local NONE_CONFIG = [[
118   --noopt-comments --noopt-whitespace --noopt-emptylines
119   --noopt-eols --noopt-strings --noopt-numbers
120   --noopt-locals
121 ]]
122
123 local DEFAULT_SUFFIX = "_"      -- default suffix for file renaming
124 local PLUGIN_SUFFIX = "plugin/" -- relative location of plugins
125
126 --[[--------------------------------------------------------------------
127 -- startup and initialize option list handling
128 ----------------------------------------------------------------------]]
129
130 -- simple error message handler; change to error if traceback wanted
131 local function die(msg)
132   print("LuaSrcDiet: "..msg); os.exit()
133 end
134 --die = error--DEBUG
135
136 if not string.match(_VERSION, "5.1", 1, 1) then  -- sanity check
137   die("requires Lua 5.1 to run")
138 end
139
140 ------------------------------------------------------------------------
141 -- prepares text for list of optimizations, prepare lookup table
142 ------------------------------------------------------------------------
143
144 local MSG_OPTIONS = ""
145 do
146   local WIDTH = 24
147   local o = {}
148   for op, desc in gmatch(OPTION, "%s*([^,]+),'([^']+)'") do
149     local msg = "  "..op
150     msg = msg..string.rep(" ", WIDTH - #msg)..desc.."\n"
151     MSG_OPTIONS = MSG_OPTIONS..msg
152     o[op] = true
153     o["--no"..sub(op, 3)] = true
154   end
155   OPTION = o  -- replace OPTION with lookup table
156 end
157
158 MSG_USAGE = string.format(MSG_USAGE, MSG_OPTIONS, DEFAULT_CONFIG)
159
160 ------------------------------------------------------------------------
161 -- global variable initialization, option set handling
162 ------------------------------------------------------------------------
163
164 local suffix = DEFAULT_SUFFIX           -- file suffix
165 local option = {}                       -- program options
166 local stat_c, stat_l                    -- statistics tables
167
168 -- function to set option lookup table based on a text list of options
169 -- note: additional forced settings for --opt-eols is done in optlex.lua
170 local function set_options(CONFIG)
171   for op in gmatch(CONFIG, "(%-%-%S+)") do
172     if sub(op, 3, 4) == "no" and        -- handle negative options
173        OPTION["--"..sub(op, 5)] then
174       option[sub(op, 5)] = false
175     else
176       option[sub(op, 3)] = true
177     end
178   end
179 end
180
181 --[[--------------------------------------------------------------------
182 -- support functions
183 ----------------------------------------------------------------------]]
184
185 -- list of token types, parser-significant types are up to TTYPE_GRAMMAR
186 -- while the rest are not used by parsers; arranged for stats display
187 local TTYPES = {
188   "TK_KEYWORD", "TK_NAME", "TK_NUMBER",         -- grammar
189   "TK_STRING", "TK_LSTRING", "TK_OP",
190   "TK_EOS",
191   "TK_COMMENT", "TK_LCOMMENT",                  -- non-grammar
192   "TK_EOL", "TK_SPACE",
193 }
194 local TTYPE_GRAMMAR = 7
195
196 local EOLTYPES = {                      -- EOL names for token dump
197   ["\n"] = "LF", ["\r"] = "CR",
198   ["\n\r"] = "LFCR", ["\r\n"] = "CRLF",
199 }
200
201 ------------------------------------------------------------------------
202 -- read source code from file
203 ------------------------------------------------------------------------
204
205 local function load_file(fname)
206   local INF = io.open(fname, "rb")
207   if not INF then die("cannot open \""..fname.."\" for reading") end
208   local dat = INF:read("*a")
209   if not dat then die("cannot read from \""..fname.."\"") end
210   INF:close()
211   return dat
212 end
213
214 ------------------------------------------------------------------------
215 -- save source code to file
216 ------------------------------------------------------------------------
217
218 local function save_file(fname, dat)
219   local OUTF = io.open(fname, "wb")
220   if not OUTF then die("cannot open \""..fname.."\" for writing") end
221   local status = OUTF:write(dat)
222   if not status then die("cannot write to \""..fname.."\"") end
223   OUTF:close()
224 end
225
226 ------------------------------------------------------------------------
227 -- functions to deal with statistics
228 ------------------------------------------------------------------------
229
230 -- initialize statistics table
231 local function stat_init()
232   stat_c, stat_l = {}, {}
233   for i = 1, #TTYPES do
234     local ttype = TTYPES[i]
235     stat_c[ttype], stat_l[ttype] = 0, 0
236   end
237 end
238
239 -- add a token to statistics table
240 local function stat_add(tok, seminfo)
241   stat_c[tok] = stat_c[tok] + 1
242   stat_l[tok] = stat_l[tok] + #seminfo
243 end
244
245 -- do totals for statistics table, return average table
246 local function stat_calc()
247   local function avg(c, l)                      -- safe average function
248     if c == 0 then return 0 end
249     return l / c
250   end
251   local stat_a = {}
252   local c, l = 0, 0
253   for i = 1, TTYPE_GRAMMAR do                   -- total grammar tokens
254     local ttype = TTYPES[i]
255     c = c + stat_c[ttype]; l = l + stat_l[ttype]
256   end
257   stat_c.TOTAL_TOK, stat_l.TOTAL_TOK = c, l
258   stat_a.TOTAL_TOK = avg(c, l)
259   c, l = 0, 0
260   for i = 1, #TTYPES do                         -- total all tokens
261     local ttype = TTYPES[i]
262     c = c + stat_c[ttype]; l = l + stat_l[ttype]
263     stat_a[ttype] = avg(stat_c[ttype], stat_l[ttype])
264   end
265   stat_c.TOTAL_ALL, stat_l.TOTAL_ALL = c, l
266   stat_a.TOTAL_ALL = avg(c, l)
267   return stat_a
268 end
269
270 --[[--------------------------------------------------------------------
271 -- main tasks
272 ----------------------------------------------------------------------]]
273
274 ------------------------------------------------------------------------
275 -- a simple token dumper, minimal translation of seminfo data
276 ------------------------------------------------------------------------
277
278 local function dump_tokens(srcfl)
279   --------------------------------------------------------------------
280   -- load file and process source input into tokens
281   --------------------------------------------------------------------
282   local z = load_file(srcfl)
283   llex.init(z)
284   llex.llex()
285   local toklist, seminfolist = llex.tok, llex.seminfo
286   --------------------------------------------------------------------
287   -- display output
288   --------------------------------------------------------------------
289   for i = 1, #toklist do
290     local tok, seminfo = toklist[i], seminfolist[i]
291     if tok == "TK_OP" and string.byte(seminfo) < 32 then
292       seminfo = "(".. string.byte(seminfo)..")"
293     elseif tok == "TK_EOL" then
294       seminfo = EOLTYPES[seminfo]
295     else
296       seminfo = "'"..seminfo.."'"
297     end
298     print(tok.." "..seminfo)
299   end--for
300 end
301
302 ----------------------------------------------------------------------
303 -- parser dump; dump globalinfo and localinfo tables
304 ----------------------------------------------------------------------
305
306 local function dump_parser(srcfl)
307   local print = print
308   --------------------------------------------------------------------
309   -- load file and process source input into tokens
310   --------------------------------------------------------------------
311   local z = load_file(srcfl)
312   llex.init(z)
313   llex.llex()
314   local toklist, seminfolist, toklnlist
315     = llex.tok, llex.seminfo, llex.tokln
316   --------------------------------------------------------------------
317   -- do parser optimization here
318   --------------------------------------------------------------------
319   lparser.init(toklist, seminfolist, toklnlist)
320   local globalinfo, localinfo = lparser.parser()
321   --------------------------------------------------------------------
322   -- display output
323   --------------------------------------------------------------------
324   local hl = string.rep("-", 72)
325   print("*** Local/Global Variable Tracker Tables ***")
326   print(hl.."\n GLOBALS\n"..hl)
327   -- global tables have a list of xref numbers only
328   for i = 1, #globalinfo do
329     local obj = globalinfo[i]
330     local msg = "("..i..") '"..obj.name.."' -> "
331     local xref = obj.xref
332     for j = 1, #xref do msg = msg..xref[j].." " end
333     print(msg)
334   end
335   -- local tables have xref numbers and a few other special
336   -- numbers that are specially named: decl (declaration xref),
337   -- act (activation xref), rem (removal xref)
338   print(hl.."\n LOCALS (decl=declared act=activated rem=removed)\n"..hl)
339   for i = 1, #localinfo do
340     local obj = localinfo[i]
341     local msg = "("..i..") '"..obj.name.."' decl:"..obj.decl..
342                 " act:"..obj.act.." rem:"..obj.rem
343     if obj.isself then
344       msg = msg.." isself"
345     end
346     msg = msg.." -> "
347     local xref = obj.xref
348     for j = 1, #xref do msg = msg..xref[j].." " end
349     print(msg)
350   end
351   print(hl.."\n")
352 end
353
354 ------------------------------------------------------------------------
355 -- reads source file(s) and reports some statistics
356 ------------------------------------------------------------------------
357
358 local function read_only(srcfl)
359   local print = print
360   --------------------------------------------------------------------
361   -- load file and process source input into tokens
362   --------------------------------------------------------------------
363   local z = load_file(srcfl)
364   llex.init(z)
365   llex.llex()
366   local toklist, seminfolist = llex.tok, llex.seminfo
367   print(MSG_TITLE)
368   print("Statistics for: "..srcfl.."\n")
369   --------------------------------------------------------------------
370   -- collect statistics
371   --------------------------------------------------------------------
372   stat_init()
373   for i = 1, #toklist do
374     local tok, seminfo = toklist[i], seminfolist[i]
375     stat_add(tok, seminfo)
376   end--for
377   local stat_a = stat_calc()
378   --------------------------------------------------------------------
379   -- display output
380   --------------------------------------------------------------------
381   local fmt = string.format
382   local function figures(tt)
383     return stat_c[tt], stat_l[tt], stat_a[tt]
384   end
385   local tabf1, tabf2 = "%-16s%8s%8s%10s", "%-16s%8d%8d%10.2f"
386   local hl = string.rep("-", 42)
387   print(fmt(tabf1, "Lexical",  "Input", "Input", "Input"))
388   print(fmt(tabf1, "Elements", "Count", "Bytes", "Average"))
389   print(hl)
390   for i = 1, #TTYPES do
391     local ttype = TTYPES[i]
392     print(fmt(tabf2, ttype, figures(ttype)))
393     if ttype == "TK_EOS" then print(hl) end
394   end
395   print(hl)
396   print(fmt(tabf2, "Total Elements", figures("TOTAL_ALL")))
397   print(hl)
398   print(fmt(tabf2, "Total Tokens", figures("TOTAL_TOK")))
399   print(hl.."\n")
400 end
401
402 ------------------------------------------------------------------------
403 -- process source file(s), write output and reports some statistics
404 ------------------------------------------------------------------------
405
406 local function process_file(srcfl, destfl)
407   local function print(...)             -- handle quiet option
408     if option.QUIET then return end
409     _G.print(...)
410   end
411   if plugin and plugin.init then        -- plugin init
412     option.EXIT = false
413     plugin.init(option, srcfl, destfl)
414     if option.EXIT then return end
415   end
416   print(MSG_TITLE)                      -- title message
417   --------------------------------------------------------------------
418   -- load file and process source input into tokens
419   --------------------------------------------------------------------
420   local z = load_file(srcfl)
421   if plugin and plugin.post_load then   -- plugin post-load
422     z = plugin.post_load(z) or z
423     if option.EXIT then return end
424   end
425   llex.init(z)
426   llex.llex()
427   local toklist, seminfolist, toklnlist
428     = llex.tok, llex.seminfo, llex.tokln
429   if plugin and plugin.post_lex then    -- plugin post-lex
430     plugin.post_lex(toklist, seminfolist, toklnlist)
431     if option.EXIT then return end
432   end
433   --------------------------------------------------------------------
434   -- collect 'before' statistics
435   --------------------------------------------------------------------
436   stat_init()
437   for i = 1, #toklist do
438     local tok, seminfo = toklist[i], seminfolist[i]
439     stat_add(tok, seminfo)
440   end--for
441   local stat1_a = stat_calc()
442   local stat1_c, stat1_l = stat_c, stat_l
443   --------------------------------------------------------------------
444   -- do parser optimization here
445   --------------------------------------------------------------------
446   if option["opt-locals"] then
447     optparser.print = print  -- hack
448     lparser.init(toklist, seminfolist, toklnlist)
449     local globalinfo, localinfo = lparser.parser()
450     if plugin and plugin.post_parse then        -- plugin post-parse
451       plugin.post_parse(globalinfo, localinfo)
452       if option.EXIT then return end
453     end
454     optparser.optimize(option, toklist, seminfolist, globalinfo, localinfo)
455     if plugin and plugin.post_optparse then     -- plugin post-optparse
456       plugin.post_optparse()
457       if option.EXIT then return end
458     end
459   end
460   --------------------------------------------------------------------
461   -- do lexer optimization here, save output file
462   --------------------------------------------------------------------
463   optlex.print = print  -- hack
464   toklist, seminfolist, toklnlist
465     = optlex.optimize(option, toklist, seminfolist, toklnlist)
466   if plugin and plugin.post_optlex then         -- plugin post-optlex
467     plugin.post_optlex(toklist, seminfolist, toklnlist)
468     if option.EXIT then return end
469   end
470   local dat = table.concat(seminfolist)
471   -- depending on options selected, embedded EOLs in long strings and
472   -- long comments may not have been translated to \n, tack a warning
473   if string.find(dat, "\r\n", 1, 1) or
474      string.find(dat, "\n\r", 1, 1) then
475     optlex.warn.mixedeol = true
476   end
477   -- save optimized source stream to output file
478   save_file(destfl, dat)
479   --------------------------------------------------------------------
480   -- collect 'after' statistics
481   --------------------------------------------------------------------
482   stat_init()
483   for i = 1, #toklist do
484     local tok, seminfo = toklist[i], seminfolist[i]
485     stat_add(tok, seminfo)
486   end--for
487   local stat_a = stat_calc()
488   --------------------------------------------------------------------
489   -- display output
490   --------------------------------------------------------------------
491   print("Statistics for: "..srcfl.." -> "..destfl.."\n")
492   local fmt = string.format
493   local function figures(tt)
494     return stat1_c[tt], stat1_l[tt], stat1_a[tt],
495            stat_c[tt],  stat_l[tt],  stat_a[tt]
496   end
497   local tabf1, tabf2 = "%-16s%8s%8s%10s%8s%8s%10s",
498                        "%-16s%8d%8d%10.2f%8d%8d%10.2f"
499   local hl = string.rep("-", 68)
500   print("*** lexer-based optimizations summary ***\n"..hl)
501   print(fmt(tabf1, "Lexical",
502             "Input", "Input", "Input",
503             "Output", "Output", "Output"))
504   print(fmt(tabf1, "Elements",
505             "Count", "Bytes", "Average",
506             "Count", "Bytes", "Average"))
507   print(hl)
508   for i = 1, #TTYPES do
509     local ttype = TTYPES[i]
510     print(fmt(tabf2, ttype, figures(ttype)))
511     if ttype == "TK_EOS" then print(hl) end
512   end
513   print(hl)
514   print(fmt(tabf2, "Total Elements", figures("TOTAL_ALL")))
515   print(hl)
516   print(fmt(tabf2, "Total Tokens", figures("TOTAL_TOK")))
517   print(hl)
518   --------------------------------------------------------------------
519   -- report warning flags from optimizing process
520   --------------------------------------------------------------------
521   if optlex.warn.lstring then
522     print("* WARNING: "..optlex.warn.lstring)
523   elseif optlex.warn.mixedeol then
524     print("* WARNING: ".."output still contains some CRLF or LFCR line endings")
525   end
526   print()
527 end
528
529 --[[--------------------------------------------------------------------
530 -- main functions
531 ----------------------------------------------------------------------]]
532
533 local arg = {...}  -- program arguments
534 local fspec = {}
535 set_options(DEFAULT_CONFIG)     -- set to default options at beginning
536
537 ------------------------------------------------------------------------
538 -- per-file handling, ship off to tasks
539 ------------------------------------------------------------------------
540
541 local function do_files(fspec)
542   for _, srcfl in ipairs(fspec) do
543     local destfl
544     ------------------------------------------------------------------
545     -- find and replace extension for filenames
546     ------------------------------------------------------------------
547     local extb, exte = string.find(srcfl, "%.[^%.%\\%/]*$")
548     local basename, extension = srcfl, ""
549     if extb and extb > 1 then
550       basename = sub(srcfl, 1, extb - 1)
551       extension = sub(srcfl, extb, exte)
552     end
553     destfl = basename..suffix..extension
554     if #fspec == 1 and option.OUTPUT_FILE then
555       destfl = option.OUTPUT_FILE
556     end
557     if srcfl == destfl then
558       die("output filename identical to input filename")
559     end
560     ------------------------------------------------------------------
561     -- perform requested operations
562     ------------------------------------------------------------------
563     if option.DUMP_LEXER then
564       dump_tokens(srcfl)
565     elseif option.DUMP_PARSER then
566       dump_parser(srcfl)
567     elseif option.READ_ONLY then
568       read_only(srcfl)
569     else
570       process_file(srcfl, destfl)
571     end
572   end--for
573 end
574
575 ------------------------------------------------------------------------
576 -- main function (entry point is after this definition)
577 ------------------------------------------------------------------------
578
579 local function main()
580   local argn, i = #arg, 1
581   if argn == 0 then
582     option.HELP = true
583   end
584   --------------------------------------------------------------------
585   -- handle arguments
586   --------------------------------------------------------------------
587   while i <= argn do
588     local o, p = arg[i], arg[i + 1]
589     local dash = string.match(o, "^%-%-?")
590     if dash == "-" then                 -- single-dash options
591       if o == "-h" then
592         option.HELP = true; break
593       elseif o == "-v" then
594         option.VERSION = true; break
595       elseif o == "-s" then
596         if not p then die("-s option needs suffix specification") end
597         suffix = p
598         i = i + 1
599       elseif o == "-o" then
600         if not p then die("-o option needs a file name") end
601         option.OUTPUT_FILE = p
602         i = i + 1
603       elseif o == "-" then
604         break -- ignore rest of args
605       else
606         die("unrecognized option "..o)
607       end
608     elseif dash == "--" then            -- double-dash options
609       if o == "--help" then
610         option.HELP = true; break
611       elseif o == "--version" then
612         option.VERSION = true; break
613       elseif o == "--keep" then
614         if not p then die("--keep option needs a string to match for") end
615         option.KEEP = p
616         i = i + 1
617       elseif o == "--plugin" then
618         if not p then die("--plugin option needs a module name") end
619         if option.PLUGIN then die("only one plugin can be specified") end
620         option.PLUGIN = p
621         plugin = require(PLUGIN_SUFFIX..p)
622         i = i + 1
623       elseif o == "--quiet" then
624         option.QUIET = true
625       elseif o == "--read-only" then
626         option.READ_ONLY = true
627       elseif o == "--basic" then
628         set_options(BASIC_CONFIG)
629       elseif o == "--maximum" then
630         set_options(MAXIMUM_CONFIG)
631       elseif o == "--none" then
632         set_options(NONE_CONFIG)
633       elseif o == "--dump-lexer" then
634         option.DUMP_LEXER = true
635       elseif o == "--dump-parser" then
636         option.DUMP_PARSER = true
637       elseif o == "--details" then
638         option.DETAILS = true
639       elseif OPTION[o] then  -- lookup optimization options
640         set_options(o)
641       else
642         die("unrecognized option "..o)
643       end
644     else
645       fspec[#fspec + 1] = o             -- potential filename
646     end
647     i = i + 1
648   end--while
649   if option.HELP then
650     print(MSG_TITLE..MSG_USAGE); return true
651   elseif option.VERSION then
652     print(MSG_TITLE); return true
653   end
654   if #fspec > 0 then
655     if #fspec > 1 and option.OUTPUT_FILE then
656       die("with -o, only one source file can be specified")
657     end
658     do_files(fspec)
659     return true
660   else
661     die("nothing to do!")
662   end
663 end
664
665 -- entry point -> main() -> do_files()
666 if not main() then
667   die("Please run with option -h or --help for usage information")
668 end
669
670 -- end of script