#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-dev" } --- Default options. candran.default = { target = _VERSION == "Lua 5.1" and "luajit" or "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