--[[ This moudle allows you to minify gLua code Use: local x = require("glum.lua") local str =" --Here is some code to be minified!\n for a=1,10,2 do\n print(a)\n end " print(x.minify(str)) Dependencies: lua-parser lpeg ]] local parser local msg if include ~= nil then parser = include("./parser.lua") msg = Msg else parser = dofile("../src/parser.lua") msg = io.write end local lpeg = require("lpeg") lpeg.locale(lpeg) local glum = {} --Creates a deep copy of a table local function deepcopy(orig) local orig_type = type(orig) local copy if orig_type == "table" then copy = {} for orig_key, orig_value in next, orig, nil do copy[deepcopy(orig_key)] = deepcopy(orig_value) end setmetatable(copy, deepcopy(getmetatable(orig))) else -- number, string, boolean, etc copy = orig end return copy end --A list of reserved words that cannot be used as variable names local nonames = {"if","for","end","do","local","then","else","elseif","return","goto","function","nil","false","true","repeat","return","break","and","or","not","in","repeat","until","while","continue"} local reservednames = {} for k,v in ipairs(nonames) do reservednames[v] = true end --A function that generates the next valid variable name from the last valid variable name. local varchars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXY" local function getnextvarname(lname) local place = string.find(lname,"[" .. varchars .. "]") local length = string.len(lname) if place == nil then return string.rep("a", length + 1) else local lbyte = string.byte(lname,place) local newchar = string.char(lbyte + (lbyte < 122 and 1 or -57)) local after = string.sub(lname, place + 1, length) local before = string.rep("a", place-1) local output = before .. newchar .. after while reservednames[output] or _G[output] do output = getnextvarname(output) end return output end end --A debugging function, a replacement for glua PrintTable local function printtable(tbl, tabset) tabset = tabset or 0 for k,v in pairs(tbl) do for i = 0,tabset do msg("\t") end msg(k .. ":") if type(v) == "table" then msg("\n") printtable(v, tabset + 1) else msg(tostring(v) .. "\n") end end end local stringreps local function findstrings(ast) if type(ast) ~= "table" then return end if ast and ast.tag == "String" then local lnum = stringreps[ast[1]] stringreps[ast[1]] = lnum and lnum + 1 or 1 return end for k = 1, #ast do findstrings(ast[k]) end end local getstringreps = function(ast) stringreps = {} findstrings(ast) local function bytessaved(str,instances,bytereplacement) local spacetaken = string.len(str) * instances local minspacetaken = instances * bytereplacement return spacetaken - minspacetaken end local sstbl = {} for k,v in pairs(stringreps) do table.insert(sstbl,{k,bytessaved(k,v,2)}) end table.sort(sstbl,function(a,b) return a[2] > b[2] end) return sstbl end local syntax = {} local function stringfor(ast,tbl) if syntax[ast.tag] ~= nil then return syntax[ast.tag](ast,tbl) else print("Valid tags are:") for k,v in pairs(syntax) do print(k) end printtable(ast) error("Attempted to use unknown tag type:" .. ast.tag) end end --Abandon all hope, ye who enter here --Refer to the comments at the top of parser.lua for what each function should do. --If anyone ever decides to add new language features, they need to be added to BOTH parser.lua and here. syntax = { ["Call"] = function(ast,tbl) local exprname = stringfor(ast[1],tbl) local argnames = {} local cursor = 2 while ast[cursor] ~= nil do argnames[cursor-1] = stringfor(ast[cursor],tbl) cursor = cursor + 1 end local argstring = table.concat(argnames,",") local ostring = table.concat({exprname,"(",argstring,")"}) return ostring end, ["Invoke"] = function(ast,tbl) local func = stringfor(ast[1],tbl) local invargs = {} for k = 3,#ast do invargs[#invargs + 1] = stringfor(ast[k],tbl) end local output = func local inv --A short hand if it's a simple thing if ast[2].tag == "String" and ast[2][1]:find(" ") == nil and tbl.strings[ast[2][1]] == nil then inv = ast[2][1] output = table.concat({output, ":", inv, "("}) elseif tbl.strings[ast[2][1]] ~= nil then output = table.concat({output,"[", tbl.strings[ast[2][1]], "]","("}) else inv = stringfor(ast[2],tbl) output = table.concat({output, "[", inv, "](", func, ","}) end output = output .. table.concat(invargs,",") .. ")" return output end, ["String"] = function(ast,tbl) local sop,eop = "\"","\"" print("looking for",ast[1],"in") printtable(tbl.strings) if tbl.strings[ast[1]] then print("Found it, it is", tbl.strings[ast[1]]) return tbl.strings[ast[1]] end if tbl.strings[ast[1]] == nil then if string.find(ast[1],"\"") then sop = "[[" eop = "]]" end return table.concat({sop,ast[1],eop}) end print("Returning non-catated string") return tbl.strings[ast[1]] end, ["Id"] = function(ast,tbl) if tbl.ids[ast[1]] == nil then return ast[1] end return tbl.ids[ast[1]] end, ["Index"] = function(ast,tbl) local globalvar = stringfor(ast[1],tbl) if ast[2].tag == "String" then return table.concat({globalvar, ".", ast[2][1]}) end return table.concat({globalvar, "[", stringfor(ast[2],tbl), "]"}) end, ["Paren"] = function(ast,tbl) return table.concat({"(",stringfor(ast[1],tbl),")"}) end, ["Dots"] = function(ast,tbl) return "..." end, ["Forin"] = function(ast,tbl) local nadd = deepcopy(tbl) local nl = stringfor(ast[1],nadd) nadd.numlocals = nadd.numlocals + #ast[1] print("Found",#ast[1],"locals as Forin") local el = stringfor(ast[2],nadd) local code = stringfor(ast[3],nadd) local output = table.concat({" for ", nl, " in ", el, " do ", code, " end "}) tbl.numlocals = nadd.numlocals return output end, ["NameList"] = function(ast,tbl) local outputtbl = {} for k = 1,#ast do if ast[k].tag ~= "Id" then outputtbl[#outputtbl + 1] = stringfor(ast[k]) else if tbl.ids[ast[k][1]] ~= nil then outputtbl[#outputtbl + 1] = tbl.ids[ast[k][1]] else local newvar = getnextvarname(tbl.lname) tbl.lname = newvar tbl.ids[ast[k][1]] = newvar outputtbl[#outputtbl + 1] = newvar end end end local output = table.concat(outputtbl, ",") return output end, ["ExpList"] = function(ast,tbl) local exprs = {} for k = 1,#ast do exprs[k] = stringfor(ast[k],tbl) end return table.concat(exprs,",") end, ["Nil"] = function(ast,tbl) return " nil " end, ["True"] = function(ast,tbl) return " true " end, ["False"] = function(ast,tbl) return " false " end, ["Return"] = function(ast,tbl) local retargs = {} for k,v in ipairs(ast) do retargs[k] = stringfor(v,tbl) end return " return " .. table.concat(retargs,",") end, ["Do"] = function(ast,tbl) local ntbl = deepcopy(tbl) local allst = {} for k = 1,#ast do allst[k] = stringfor(ast[k],ntbl) end local code = table.concat(allst,";") tbl.numlocals = ntbl.numlocals return table.concat({" do ", code," end "}) end, ["If"] = function(ast,tbl) local expr1 = stringfor(ast[1],tbl) local block1 = stringfor(ast[2],tbl) local codeblocks = {} codeblocks[#codeblocks + 1] = table.concat({" if ",expr1," then ",block1}) for k = 3,#ast-1,2 do local expr = stringfor(ast[k],tbl) local block = stringfor(ast[k + 1],tbl) codeblocks[#codeblocks + 1] = table.concat({" elseif " , expr , " then " , block}) end if #ast % 2 == 1 then local block = stringfor(ast[#ast],tbl) if block ~= "" then --If for some reason there's an empty else block, forget about it. codeblocks[#codeblocks + 1] = " else " .. block end end codeblocks[#codeblocks + 1] = " end " return table.concat(codeblocks) end, ["Fornum"] = function(ast,tbl) local var if ast[1].tag == "Id" then if tbl.ids[ast[1][1]] ~= nil then var = tbl.ids[ast[1][1]] else local newvar = getnextvarname(tbl.lname) tbl.lname = newvar tbl.ids[ast[1][1]] = newvar var = newvar end else var = stringfor(ast[1],tbl) end local start = stringfor(ast[2],tbl) local endnum = stringfor(ast[3],tbl) local incrementer = 1 local code = "" if ast[4].tag ~= "Block" then -- incrementer incrementer = stringfor(ast[4],tbl) code = stringfor(ast[5],tbl) else code = stringfor(ast[4],tbl) end local incstr = incrementer ~= 1 and ("," .. incrementer) or "" tbl[var] = nil tbl.numlocals = tbl.numlocals + 1 print("Found 1 locals as Fornum") return table.concat({" for ",var,"=",start,",",endnum,incstr," do ",code," end "}) end, ["Op"] = function(ast,tbl) local binop = { ["or"] = " or ", ["and"] = " and ", ["ne"] = "~=", ["eq"] = "==", ["le"] = "<=", ["ge"] = ">=", ["lt"] = "<", ["gt"] = ">", ["bor"] = "|", ["bxor"] = "~", ["band"] = "&", ["shl"] = "<<", ["shr"] = ">>", ["concat"] = "..", ["add"] = "+", ["sub"] = "-", ["mul"] = "*", ["div"] = "/", ["mod"] = "%", ["pow"] = "^", } local uniop = { ["len"] = "#", ["not"] = " not ", ["unm"] = "-", ["bnot"] = "~", } local opname = ast[1] if uniop[opname] ~= nil then --Some special case where the parser messes up, fix it here. --It translates ~= into not ==, but the order of operations makes it so == is evaluated first, and not second. if opname == "not" and ast[2]["tag"] == "Op" and ast[2][1] == "eq" then ast[2][1] = "ne" return stringfor(ast[2],tbl) end local rhs = stringfor(ast[2],tbl) return uniop[opname] .. rhs end local lhs = stringfor(ast[2],tbl) local rhs = stringfor(ast[3],tbl) local output = table.concat({lhs,binop[opname],rhs}) return output end, ["Pair"] = function(ast,tbl) local lhs = stringfor(ast[1],tbl) local rhs = stringfor(ast[2],tbl) return table.concat({"[",lhs,"]=",rhs}) end, ["Table"] = function(ast,tbl) local fields = {} for k = 1, #ast do fields[#fields + 1] = stringfor(ast[k],tbl) end local fieldstr = table.concat(fields,",") return table.concat({"{",fieldstr,"}"}) end, ["Number"] = function(ast,tbl) return ast[1] end, ["Local"] = function(ast,tbl) local lhs,rhs = stringfor(ast[1],tbl),nil tbl.numlocals = tbl.numlocals + #ast[1] print("Found",#ast[1],"locals as Local") if ast[2].tag ~= nil then rhs = stringfor(ast[2],tbl) end local output = "local " .. lhs if ast[2].tag ~= nil then output = output .. "=" .. rhs .. ";" end return output end, ["VarList"] = function(ast,tbl) local vars = {} for k = 1,#ast do vars[#vars + 1] = stringfor(ast[k],tbl) end return table.concat(vars,",") end, ["Set"] = function(ast,tbl) local lhs = {} local a1 = ast[1].tag ~= nil and ast[1] or ast[1][1] for k = 1,#ast[1] do lhs[#lhs + 1] = stringfor(a1,tbl) end local rhs = {} local a2 = ast[2].tag ~= nil and ast[2] or ast[2][1] for k = 1,#ast[2] do rhs[#rhs + 1] = stringfor(a2,tbl) end local ostring = table.concat(lhs,",") ostring = ostring .. "=" .. table.concat(rhs,",") return ostring .. ";" end, ["Label"] = function(ast,tbl) if tbl.nids[ast[1]] == nil then local nextvar = getnextvarname(tbl.lname) tbl.lname = nextvar tbl.nids[ast[1]] = nextvar end return "::" .. tbl.nids[ast[1]] .. "::" end, ["Goto"] = function(ast,tbl) if tbl.nids[ast[1]] == nil then local nextvar = getnextvarname(tbl.lname) tbl.lname = nextvar tbl.nids[ast[1]] = nextvar end return " goto " .. tbl.nids[ast[1]] end, ["Function"] = function(ast,tbl) local funcargs = ast[1].tag ~= nil and stringfor(ast[1],tbl) or "" local code = stringfor(ast[2],tbl) return table.concat({" function(",funcargs,")",code," end "}) end, ["Localrec"] = function(ast,tbl) local ident if tbl.ids[ast[1][1]] ~= nil then ident = tbl.ids[ast[1][1]] else local newvar = getnextvarname(tbl.lname) tbl.lname = newvar tbl.ids[ast[1][1][1]] = newvar ident = newvar end local argstr = ast[2][1][1].tag ~= nil and stringfor(ast[2][1][1],tbl) or "" local expr = stringfor(ast[2][1][2],tbl) tbl.numlocals = tbl.numlocals + 1 print("Found 1 local as Localrec") return table.concat({" local function ",ident,"(",argstr,")",expr," end "}) end, ["Continue"] = function(ast,tbl) return " continue " end, ["While"] = function(ast,tbl) local expr = stringfor(ast[1],tbl) local block = stringfor(ast[2],tbl) local output = table.concat({" while " , expr , " do " , block , " end "}) return output end, ["Break"] = function(ast,tbl) return " break " end, ["Block"] = function(ast,oldtbl) local tbl = deepcopy(oldtbl) oldtbl.block = true local codeblocks = {} for k = 1,#ast do codeblocks[#codeblocks + 1] = stringfor(ast[k],tbl) end local code = table.concat(codeblocks) oldtbl.numlocals = tbl.numlocals return code end, } --Removes extra spaces and duplicated ; from a string local function removespaces(str) local removables = { {"%s*%)%s*","%)"}, --Spaces before or after ) {"%s*%(%s*","%("}, --Spaces before or after ( {"%s*;%s*",";"}, --Spaces before or after ; {"%s*,%s*",","}, --Spaces before or after , {";+",";"}, --Multiple ; in a row {"^%s*",""}, --Spaces at the beginning of the file {"%s*$",""}, --Spaces at the end of the file {"%s+"," "}, --Multiple spaces in a row } --Order is important for k,v in ipairs(removables) do str = string.gsub(str,v[1],v[2]) end return str end --Compress the string, and adds a little decompression code at the top. --local function compress(str) -- --end glum.minify = function(str, name) name = name or "anonymous" local ast, error_msg = parser.parse(str, name) if not ast then error(error_msg) return nil end print("Finding string reps") local strreps = getstringreps(ast) printtable(strreps) local olocalvar = { ["numlocals"] = 0, ["strings"] = {}, ["ids"] = {}, ["lname"] = "", ["nids"] = {}, } --printtable(ast) local lvt = deepcopy(olocalvar) local ret = stringfor(ast,olocalvar) local numstrreplaced = 0 local maxlocals = lvt.numlocals while (numstrreplaced + maxlocals < 200) and --We have some locals left (numstrreplaced < #strreps) and --We have more strings to replace (strreps[numstrreplaced+1][2] > 5) do --Replaceing this string will at least cover the cost of "local " numstrreplaced = numstrreplaced + 1 local nvar = getnextvarname(olocalvar.lname) olocalvar.strings[strreps[numstrreplaced][1]] = nvar olocalvar.lname = nvar end local lhss,rhss = {},{} for k,v in pairs(olocalvar.strings) do lhss[#lhss + 1] = v rhss[#rhss + 1] = string.format("%q",k) end local inits = "" print("lhss is") printtable(lhss) local lhs = " local " .. table.concat(lhss,",") local rhs = table.concat(rhss,",") if string.len(rhs) > 0 then inits = table.concat({lhs, "=", rhs, ";"}) end print("Before doing stringfor for the second time, olocalvar is") printtable(olocalvar) return inits .. stringfor(ast,olocalvar) end glum.uglify = function(str) assert(str ~= nil, "Cannot uglify a nil string") local avalchars = {} local capture_chars = {"%","(","[","\13"} local skipchars = {} for k,v in pairs(capture_chars) do skipchars[string.byte(v,1)] = true end for k = 1, 128 do if skipchars[k] then goto skip_loop end --Skip the % character if string.find(str,string.char(k)) then avalchars[k] = false else avalchars[k] = true end ::skip_loop:: end for k,v in pairs(skipchars) do avalchars[k] = false end local prettifycode = [[ local function p(s) local r = { %s } for k,v in pairs(r) do s = s:gsub(v[2],v[1]) end return s end ]] local replacementtbl = {} local cursor = 1 for k,v in pairs(nonames) do while not avalchars[cursor] do cursor = cursor + 1 end replacementtbl[v] = cursor avalchars[cursor] = false end assert(cursor < 128, "Unable to uglify file, not enough unused characters!") local replacementstr = {} for k,v in pairs(replacementtbl) do local trs = string.format("{%q,%q}",k,string.char(v)) replacementstr[#replacementstr + 1] = trs end local frepstr = table.concat(replacementstr,",") local pcd = string.format(prettifycode,frepstr) for k,v in pairs(replacementtbl) do str = str:gsub(k,string.char(v)) end local numdeepcom = math.random(5) + 5 return table.concat({ pcd, "\n", "return assert(loadstring(p([", string.rep("=",numdeepcom), "[", str, "]", string.rep("=",numdeepcom), "])))()" }) --prettifycode = string.format(prettifycode,) end return glum