mirror of
https://github.com/Reuh/candran.git
synced 2025-10-27 09:59:29 +00:00
278 lines
8.3 KiB
Text
278 lines
8.3 KiB
Text
#import("lib.util")
|
|
#import("lib.cmdline")
|
|
|
|
#import("compiler.lua53")
|
|
#import("compiler.luajit")
|
|
|
|
#import("lib.lua-parser.scope")
|
|
#import("lib.lua-parser.validator")
|
|
#import("lib.lua-parser.pp")
|
|
#import("lib.lua-parser.parser")
|
|
|
|
local candran = {
|
|
VERSION = "0.7.0"
|
|
}
|
|
|
|
--- Default options.
|
|
candran.default = {
|
|
target = "lua53",
|
|
indentation = "",
|
|
newline = "\n",
|
|
variablePrefix = "__CAN_",
|
|
mapLines = true,
|
|
chunkname = "nil",
|
|
rewriteErrors = true
|
|
}
|
|
|
|
--- Run the preprocessor
|
|
-- @tparam input string input code
|
|
-- @tparam options table arguments for the preprocessor. They will be inserted into the preprocessor environement.
|
|
-- @treturn output string output code
|
|
function candran.preprocess(input, options={})
|
|
options = util.merge(candran.default, options)
|
|
|
|
-- generate preprocessor code
|
|
local preprocessor = ""
|
|
local i = 0
|
|
local inLongString = false
|
|
local inComment = false
|
|
for line in (input.."\n"):gmatch("(.-\n)") do
|
|
i += 1
|
|
-- Simple multiline comment/string detection
|
|
if inComment then
|
|
inComment = not line:match("%]%]")
|
|
elseif inLongString then
|
|
inLongString = not line:match("%]%]")
|
|
else
|
|
if line:match("[^%-]%[%[") then
|
|
inLongString = true
|
|
elseif line:match("%-%-%[%[") then
|
|
inComment = true
|
|
end
|
|
end
|
|
if not inComment and not inLongString and line:match("^%s*#") and not line:match("^#!") then -- exclude shebang
|
|
preprocessor ..= line:gsub("^%s*#", "")
|
|
else
|
|
local l = line:sub(1, -2)
|
|
if not inLongString and options.mapLines and not l:match("%-%- (.-)%:(%d+)$") then
|
|
preprocessor ..= ("write(%q)"):format(l .. " -- "..options.chunkname..":" .. i) .. "\n"
|
|
else
|
|
preprocessor ..= ("write(%q)"):format(line:sub(1, -2)) .. "\n"
|
|
end
|
|
end
|
|
end
|
|
preprocessor ..= "return output"
|
|
|
|
-- make preprocessor environement
|
|
local env = util.merge(_G, options)
|
|
--- Candran library table
|
|
env.candran = candran
|
|
--- Current preprocessor output
|
|
env.output = ""
|
|
--- Import an external Candran/Lua module into the generated file
|
|
-- Notable options:
|
|
-- * loadLocal (true): true to automatically load the module into a local variable
|
|
-- * loadPackage (true): true to automatically load the module into the loaded packages table
|
|
-- @tparam modpath string module path
|
|
-- @tparam margs table preprocessor options to use when preprocessessing the module
|
|
env.import = function(modpath, margs={})
|
|
local filepath = assert(util.search(modpath), "No module named \""..modpath.."\"")
|
|
|
|
-- open module file
|
|
local f = io.open(filepath)
|
|
if not f then error("Can't open the module file to import") end
|
|
|
|
margs = util.merge(options, { chunkname = filepath, loadLocal = true, loadPackage = true }, margs)
|
|
local modcontent = candran.preprocess(f:read("*a"), margs)
|
|
f:close()
|
|
|
|
-- get module name (ex: module name of path.to.module is module)
|
|
local modname = modpath:match("[^%.]+$")
|
|
|
|
env.write(
|
|
"-- MODULE "..modpath.." --\n"..
|
|
"local function _()\n"..
|
|
modcontent.."\n"..
|
|
"end\n"..
|
|
(margs.loadLocal and ("local %s = _() or %s\n"):format(modname, modname) or "").. -- auto require
|
|
(margs.loadPackage and ("package.loaded[%q] = %s or true\n"):format(modpath, margs.loadLocal and modname or "_()") or "").. -- add to package.loaded
|
|
"-- END OF MODULE "..modpath.." --"
|
|
)
|
|
end
|
|
--- Include another file content in the preprocessor output.
|
|
-- @tparam file string filepath
|
|
env.include = function(file)
|
|
local f = io.open(file)
|
|
if not f then error("Can't open the file "..file.." to include") end
|
|
env.write(f:read("*a"))
|
|
f:close()
|
|
end
|
|
--- Write a line in the preprocessor output.
|
|
-- @tparam ... string strings to write (similar to print)
|
|
env.write = function(...)
|
|
env.output ..= table.concat({...}, "\t") .. "\n"
|
|
end
|
|
--- Will be replaced with the content of the variable with the given name, if it exists.
|
|
-- @tparam name string variable name
|
|
env.placeholder = function(name)
|
|
if env[name] then
|
|
env.write(env[name])
|
|
end
|
|
end
|
|
|
|
-- compile & load preprocessor
|
|
local preprocess, err = util.load(candran.compile(preprocessor, args), "candran preprocessor", env)
|
|
if not preprocess then error("Error while creating Candran preprocessor: " .. err) end
|
|
|
|
-- execute preprocessor
|
|
local success, output = pcall(preprocess)
|
|
if not success then error("Error while preprocessing file: " .. output) end
|
|
|
|
return output
|
|
end
|
|
|
|
--- Run the compiler
|
|
-- @tparam input string input code
|
|
-- @tparam options table options for the compiler
|
|
-- @treturn output string output code
|
|
function candran.compile(input, options={})
|
|
options = util.merge(candran.default, options)
|
|
|
|
local ast, errmsg = parser.parse(input, options.chunkname)
|
|
|
|
if not ast then
|
|
error("Compiler: error while parsing file: "..errmsg)
|
|
end
|
|
|
|
return require("compiler."..options.target)(input, ast, options)
|
|
end
|
|
|
|
--- Preprocess & compile code
|
|
-- @tparam code string input code
|
|
-- @tparam options table arguments for the preprocessor and compiler
|
|
-- @treturn output string output code
|
|
function candran.make(code, options)
|
|
return candran.compile(candran.preprocess(code, options), options)
|
|
end
|
|
|
|
local errorRewritingActive = false
|
|
local codeCache = {}
|
|
--- Candran equivalent to the Lua 5.3's loadfile funtion.
|
|
-- Will rewrite errors by default.
|
|
function candran.loadfile(filepath, env, options)
|
|
local f, err = io.open(filepath)
|
|
if not f then error("can't open the file: "..err) end
|
|
local content = f:read("*a")
|
|
f:close()
|
|
|
|
return candran.load(content, filepath, env, options)
|
|
end
|
|
|
|
--- Candran equivalent to the Lua 5.3's load funtion.
|
|
-- Will rewrite errors by default.
|
|
function candran.load(chunk, chunkname, env, options={})
|
|
options = util.merge({ chunkname = tostring(chunkname or chunk) }, options)
|
|
|
|
codeCache[options.chunkname] = candran.make(chunk, options)
|
|
local f, err = util.load(codeCache[options.chunkname], options.chunkname, env)
|
|
|
|
-- Um. Candran isn't supposed to generate invalid Lua code, so this is a major issue.
|
|
-- This is not going to raise an error because this is supposed to behave similarly to Lua's load function.
|
|
-- But the error message will likely be useless unless you know how Candran works.
|
|
if f == nil then
|
|
return f, "Candran unexpectedly generated invalid code: "..err
|
|
end
|
|
|
|
if options.rewriteErrors == false then
|
|
return f
|
|
else
|
|
return function(...)
|
|
local params = {...}
|
|
if not errorRewritingActive then
|
|
errorRewritingActive = true
|
|
local t = { xpcall(() return f(unpack(params)) end, candran.messageHandler) }
|
|
errorRewritingActive = false
|
|
if t[1] == false then
|
|
error(t[2], 0)
|
|
end
|
|
return unpack(t, 2)
|
|
else
|
|
return f(...)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Candran equivalent to the Lua 5.3's dofile funtion.
|
|
-- Will rewrite errors by default.
|
|
function candran.dofile(filename, options)
|
|
local f, err = candran.loadfile(filename, nil, options)
|
|
|
|
if f == nil then
|
|
error(err)
|
|
else
|
|
return f()
|
|
end
|
|
end
|
|
|
|
--- Candran error message handler.
|
|
-- Use it in xpcall to rewrite stacktraces to display Candran source file lines instead of compiled Lua lines.
|
|
function candran.messageHandler(message)
|
|
return debug.traceback(message, 2):gsub("(\n?%s*)([^\n]-)%:(%d+)%:", function(indentation, source, line)
|
|
line = tonumber(line)
|
|
|
|
local originalFile
|
|
local strName = source:match("%[string \"(.-)\"%]")
|
|
if strName then
|
|
if codeCache[strName] then
|
|
originalFile = codeCache[strName]
|
|
source = strName
|
|
end
|
|
else
|
|
local fi = io.open(source, "r")
|
|
if fi then
|
|
originalFile = fi:read("*a")
|
|
fi:close()
|
|
end
|
|
end
|
|
|
|
if originalFile then
|
|
local i = 0
|
|
for l in originalFile:gmatch("([^\n]*)") do
|
|
i = i +1
|
|
if i == line then
|
|
local extSource, lineMap = l:match("%-%- (.-)%:(%d+)$")
|
|
if lineMap then
|
|
if extSource ~= source then
|
|
return indentation .. extSource .. ":" .. lineMap .. "(" .. extSource .. ":" .. line .. "):"
|
|
else
|
|
return indentation .. extSource .. ":" .. lineMap .. "(" .. line .. "):"
|
|
end
|
|
end
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
|
|
--- Candran package searcher function. Use the existing package.path.
|
|
function candran.searcher(modpath)
|
|
local filepath = util.search(modpath, {"can"})
|
|
if not filepath then
|
|
return "\n\tno candran file in package.path"
|
|
end
|
|
return candran.loadfile(filepath)
|
|
end
|
|
|
|
--- Register the Candran package searcher.
|
|
function candran.setup()
|
|
if _VERSION == "Lua 5.1" then
|
|
table.insert(package.loaders, 2, candran.searcher)
|
|
else
|
|
table.insert(package.searchers, 2, candran.searcher)
|
|
end
|
|
return candran
|
|
end
|
|
|
|
return candran
|