Neat, it works. I think.

This commit is contained in:
Jane Roxanne 2019-11-05 14:47:42 -05:00
parent a3fd62328e
commit 7b9d701e6f
13 changed files with 2022 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
luacomp.lua

View File

@ -1,2 +1,5 @@
# LuaComp
A general purpose Lua preprocessor and minifier.
## Building
To build, either execute `luapreproc` or `luacomp` on src/init.lua

View File

@ -0,0 +1,2 @@
--#include "world.lua"
print("hello"..get_world())

3
examples/world.lua Normal file
View File

@ -0,0 +1,3 @@
function get_world()
return "world"
end

1527
src/argparse.lua Normal file

File diff suppressed because it is too large Load Diff

249
src/ast.lua Normal file
View File

@ -0,0 +1,249 @@
--[[
ast.lua - Generates a structure for use in preprocessing.
Copyright 2019 Adorable-Catgirl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
]]
local function nextc(f, c)
c = c or 1
return f:read(c)
end
local function peek(f, c)
c = c or 1
local z = f:read(c)
f:seek("cur", -c)
return z
end
local function skip(f, c)
c = c or 1
return f:seek("cur", c)
end
local ws = {
["\t"] = true,
[" "] = true
}
local function parse_hex(f)
local lc = " "
local hex = ""
while (48 <= lc:byte() and lc:byte() <= 57) or (97 <= lc:byte() and lc:byte() <= 102) or (65 <= lc:byte() and lc:byte() <= 70) do
lc = nextc(f)
if (48 <= lc:byte() and lc:byte()) <= 57 or (97 <= lc:byte() and lc:byte() <= 102) or (65 <= lc:byte() and lc:byte() <= 70) then
hex = hex .. lc
end
end
return tonumber(hex, 16)
end
local function parse_number(f, c)
local lc = " "
local num = c
while 48 <= lc:byte() and lc:byte() <= 57 do
lc = nextc(f)
if (48 <= lc:byte() and lc:byte() <= 57) then
num = num .. lc
end
end
return tonumber(hex, 10)
end
local esct = {
["t"] = "\t",
["n"] = "\n",
["r"] = "\r",
["\\"] = "\\\\"
}
for i=0, 9 do
esct[tostring(i)] = string.char(i)
end
local function parse_dblquote(f)
local val = ""
while peek(f) ~= "\"" do
local c = nextc(f)
if (peek(f) == "\n" or peek(f) == "\r") then
return nil, "Unexpected end of line"
end
if (c == "\\") then
if (esct[peek(f)]) then
c = esct[peek(f)]
skip(f)
else
c = nextc(f)
end
end
val = val .. c
end
skip(f)
return val
end
local function parse_snglquote(f)
local val = ""
while peek(f) ~= "\'" do
local c = nextc(f)
if (peek(f) == "\n" or peek(f) == "\r") then
return nil, "Unexpected end of line"
end
if (c == "\\") then
if (esct[peek(f)]) then
c = esct[peek(f)]
skip(f)
else
c = nextc(f)
end
end
val = val .. c
end
skip(f)
return val
end
local function parse_envarg(f)
local val = ""
while peek(f) ~= ")" do
if (peek(f) == "\n" or peek(f) == "\r") then
return nil, "Unexpected end of line"
end
val = val .. nextc(f)
end
skip(f)
return val
end
local function parse_directive(f)
local lc = "_"
local name = ""
local args = {}
local carg = ""
while not ws[lc] do
lc = nextc(f)
if (lc == "\n" or lc == "\r") then
if (lc == "\r" and peek(f) == "\n") then skip(f) end
return {type="directive", name=name}
elseif not ws[lc] then
name = name .. lc
end
end
while true do
lc = nextc(f)
if (lc == "\n" or lc == "\r") then
if (lc == "\r" and peek(f) == "\n") then skip(f) end
return {type="directive", name=name, args=args}
elseif lc == "0" and peek(f) == "x" then
skip(f)
local val = parse_hex(f)
args[#args+1] = val
elseif 48 <= lc:byte() and lc:byte() <= 57 then
local val = parse_number(f, lc)
args[#args+1] = val
elseif lc == "\"" then
local val, e = parse_dblquote(f)
if not val then return val, e end
args[#args+1] = val
elseif lc == "\'" then
local val, e = parse_snglquote(f)
if not val then return val, e end
args[#args+1] = val
elseif lc == "$" and peek(f) == "(" then
skip(f)
local val = parse_envarg(f)
if not os.getenv(val) then return nil, "Enviroment variable `"..val.."' does not exist." end
args[#args+1] = os.getenv(val)
elseif not ws[lc] then
return nil, "Syntax error"
end
end
end
local function mkast(f, n)
io.stderr:write("PROC\t",n,"\n")
local lc = " "
local lpos = 1
local ilpos = 1
local tree = {}
local code = ""
local branches = {}
local function add_code()
tree[#tree+1] = {type="code", data=code, file=n, line=lpos}
code = ""
end
local function parse_error(e)
io.stderr:write("ERROR:"..n..":"..lpos..": "..e.."\n")
os.exit(1)
end
while lc and lc ~= "" do
lc = nextc(f)
if (lc == "-" and ilpos == 1) then
if (peek(f, 2) == "-#") then --Directive
add_code()
skip(f, 2)
local d, r = parse_directive(f)
if not d then
parse_error(r)
end
d.line = lpos
d.file = n
lpos = lpos+1
tree[#tree+1] = d
else
code = code .. lc
ilpos = ilpos+1
end
elseif (lc == "$" and peek(f) == "(") then
add_code()
skip(f)
local val, e = parse_envarg(f)
if not val then
parse_error(e)
end
tree[#tree+1] = {type="envvar", var=val, file=n, line=lpos}
elseif (lc == "@" and peek(f, 2) == "[[") then
add_code()
skip(f, 2)
local val = ""
while peek(f, 2) == "]]" do
val = val .. nextc(f)
end
tree[#tree+1] = {type="lua", code=val, file=n, line=lpos}
skip(f, 2)
elseif (lc == "@" and peek(f, 2) == "[{") then
add_code()
skip(f, 2)
local val = ""
while peek(f, 2) == "}]" do
val = val .. nextc(f)
end
tree[#tree+1] = {type="lua_r", code=val, file=n, line=lpos}
skip(f, 2)
elseif (lc == "\r" or lc == "\n") then
if (lc == "\r" and peek(f) == "\n") then
skip(f)
end
lpos = lpos+1
ilpos = 1
code = code .. "\n"
else
code = code .. (lc or "")
ilpos = ilpos+1
end
end
add_code()
return tree
end

View File

@ -0,0 +1,37 @@
--[[
cfg/minifier_providers.lua - Provides minifier providers.
Copyright 2019 Adorable-Catgirl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
]]
local providers = {}
function providers.luamin(cin)
local fn = os.tmpname()
local fh = io.open(fn, "w")
fh:write(cin)
fh:close()
local lmh = io.popen("luamin -f "..fn.." 2>&1", "r")
local dat = lmh:read("*a")
local stat, _, code = lmh:close()
if (code ~= 0) then
return false, dat
end
return dat
end
function providers.none(cin)
return cin
end

View File

@ -0,0 +1,21 @@
--[[
directive_provider.lua - Provides preprocessor directives
Copyright 2019 Adorable-Catgirl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
]]
--#include "directives/define.lua"
--#include "directives/include.lua"
--#include "directives/loadmod.lua"

View File

@ -0,0 +1,4 @@
function directives.define(env, evar, val)
os.setenv(evar, val)
return true
end

View File

@ -0,0 +1,10 @@
function directives.include(env, file)
if (not os.execute("stat "..file..">/dev/null")) then
return false, "File `"..file.."' does not exist!"
end
local f = io.open(file, "r")
local fast = mkast(f, file)
local code = generate(fast)
env.code = env.code .. "\n" .. code
return true
end

View File

@ -0,0 +1,13 @@
local warned = false
function directives.loadmod(env, mod)
if not warned then
io.stderr:write("Warning: loadmod is depreciated and unsafe. The API differs from luapreproc.\n")
warned = true
end
if (not os.execute("stat "..file..">/dev/null")) then
return false, "Module `"..file.."' does not exist!"
end
local modname, func = dofile(mod)
directives[modname] = func
return true
end

89
src/generator.lua Normal file
View File

@ -0,0 +1,89 @@
--[[
generator.lua - Generates the code.
Copyright 2019 Adorable-Catgirl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
]]
local function lua_escape(code)
return code:gsub("\\", "\\\\"):gsub("\"", "\\\""):gsub("\n", "\\n")
end
local directives = {}
local function generate(ast)
local lua_code = ""
for i=1, #ast do
local leaf = ast[i]
if (leaf.type == "lua") then
lua_code = lua_code .. code
elseif (leaf.type == "directive") then
local stargs = {}
for i=1, #leaf.args do
local arg = leaf.args[i]
if (type(arg) == "string") then
stargs[i] = "\""..lua_escape(arg).."\""
elseif (type(arg) == "number") then
stargs[i] = tostring(arg)
end
end
lua_code = lua_code .. "call_directive(\""..leaf.file..":"..tostring(leaf.line).."\",\""..leaf.name.."\","..table.concat(stargs, ",")..")"
elseif (leaf.type == "envvar") then
lua_code = lua_code .. "put_env(\""..leaf.file..":"..tostring(leaf.line).."\",\""..leaf.name.."\")"
elseif (leaf.type == "code") then
lua_code = lua_code .. "put_code(\""..leaf.file..":"..tostring(leaf.line).."\",\"" .. lua_escape(leaf.data) .. "\")"
elseif (leaf.type == "lua_r") then
lua_code = lua_code .. "put_code(\""..leaf.file..":"..tostring(leaf.line).."\",tostring("..leaf.code.."))"
else
io.stderr:write("ERROR: Internal catastrophic failure, unknown type "..leaf.type.."\n")
os.exit(1)
end
end
local env = {code = ""}
local function run_away_screaming(fpos, err)
io.stdout:write("ERROR: "..fps..": "..err.."\n")
os.exit(1)
end
local function call_directive(fpos, dname, ...)
if (not directives[dname]) then
run_away_screaming(fpos, "Invalid directive name `"..dname.."'")
end
local r, er = directives[dname](env, ...)
if (not r) then
run_away_screaming(fpos, er)
end
end
local function put_env(fpos, env)
local e = os.getenv(env)
if not e then
run_away_screaming(fpos, "Enviroment variable `"..env.."' does not exist!")
end
env.code = env.code .. "\""..lua_escape(e).."\""
end
local function put_code(fpos, code)
env.code = env.code .. code --not much that can fail here...
end
local fenv = {}
for k, v in pairs(_G) do
fenv[k] = v
end
fenv._G = fenv
fenv._ENV = fenv
fenv.call_directive = call_directive
fenv.put_code = put_code
fenv.put_env = put_env
local func = assert(load(lua_code, "=(generated code)", "t", fenv))
func()
return env.code
end

63
src/init.lua Normal file
View File

@ -0,0 +1,63 @@
--[[
init.lua - Main file of LuaComp
Copyright 2019 Adorable-Catgirl
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
]]
--#include "ast.lua"
--#include "generator.lua"
--#include "directive_provider.lua"
--#include "cfg/minifier_providers.lua"
local argparse = (function()
--#include "argparse.lua"
end)()
local parser = argparse(arg[0], "A Lua preprocessor+postprocessor.")
parser:argument("input", "Input file (- for STDIN)")
parser:option("-O --output", "Output file. (- for STDOUT)", "-")
parser:option("-m --minifier", "Sets the minifier", "none")
parser:flag("-x --executable", "Makes the script an executable")
local args = parser:parse()
local file = args.input
local f
if (file ~= "-") then
if (not os.execute("stat "..file..">/dev/null")) then
io.stderr:write("ERROR: File `"..file.."' does not exist!\n")
os.exit(1)
end
f = io.open(file, "r")
else
file = io.stdin
end
local ast = mkast(f, file)
local ocode = generate(ast)
local minifier = providers[args.minifier]
local rcode = minifier(ocode)
local of
if (args.output == "-") then
of = io.stdout
else
of = io.open(args.output, "w")
end
if (args.executable) then
of:write("#!/usr/bin/env lua5.3\n")
end
of:write(rcode)
of:close()
f:close()