--*-lua-*- Set as a metalua file because it requires some metalua libs --require 'verbose_require' require 'metalua.compiler' require 'clopts' do local mfast = os.getenv 'LUA_NOSPRINGS' if mfast=='yes' or mfast=='true' then function spring_pcall(f, ...) if type(f)=='string' then f = loadstring("return "..f)() end return pcall(f, ...) end else require 'springs' function spring_pcall(...) local ring = springs.new() ring:dostring (INIT_COMPILATION_RING) st, ast = ring:pcall(...) ring:close() return st, ast end end end AST_COMPILE_ERROR_NUMBER = -1 RUNTIME_ERROR_NUMBER = -3 BYTECODE_SYNTHESE_ERROR_NUMBER = -100 -{ extension 'match' } local chunks = { } local runargs = { } local acc_chunk = |kind| function (arg) table.insert (chunks, { tag=kind, arg }) end parser = clopts { -- Chunk loading { short = 'f', long = 'file', type = 'string', action = acc_chunk 'File', usage = 'load a file to compile and/or run' }, { short = 'l', long = 'library', type = 'string', action = acc_chunk 'Library', usage = 'load a libary from the standard paths' }, { short = 'e', long = 'literal', type = 'string', action = acc_chunk 'Literal', usage = 'load a literal piece of source code' }, -- What to do with chunks { short = 'o', long = 'output', type = 'string', usage = 'set the target name of the next compiled file' }, { short = 'x', long = 'run', type = 'boolean', usage = 'execute the compiled file instead of saving it (unless -o is also used)' }, { short = 'i', long = 'interactive', type = 'boolean', usage = 'run an interactive loop after having run other files' }, -- Advanced stuff { short = 'v', long = 'verbose', type = 'boolean', usage = 'verbose mode' }, { short = 'a', long = 'print-ast', type = 'boolean', usage = 'print the AST resulting from file compilation' }, { short = 'A', long = 'print-ast-lineinfo', type = 'boolean', usage = 'print the AST resulting from file compilation, including lineinfo data' }, { short = 'b', long = 'metabugs', type = 'boolean', usage = 'show syntax errors as compile-time execution errors' }, { short = 's', long = 'sharpbang', type = 'string', usage = 'set a first line to add to compiled file, typically "#!/bin/env mlr"' }, { long = 'no-runtime', type = 'boolean', usage = "prevent the automatic requirement of metalua runtime" }, { long = '', short = 'p', type = '*', action= function (newargs) runargs=table.icat(runargs, newargs) end, usage = "pass all remaining arguments to the program" }, usage=[[ Compile and/or execute metalua programs. Parameters passed to the compiler should be prefixed with an option flag, hinting what must be done with them: take tham as file names to compile, as library names to load, as parameters passed to the running program... When option flags lack, metalua tries to adopt a "Do What I Mean" approach: - if no code (no library, no literal expression and no file) is specified, the first flag-less parameter is taken as a file name to load. - if no code and no parameter is passed, an interactive loop is started. - if a target file is specified with --output, the program is not executed by default, unless a --run flag forces it to. Conversely, if no --output target is specified, the code is run unless ++run forbids it. ]]} INIT_COMPILATION_RING = [[require 'metalua.compiler']] local function main (...) local cfg = parser(...) ------------------------------------------------------------------- -- Print messages if in verbose mode ------------------------------------------------------------------- local function verb_print (fmt, ...) if cfg.verbose then return printf ("[ "..fmt.." ]", ...) end end ------------------------------------------------------------------- -- If there's no chunk but there are params, interpret the first -- param as a file name. if #chunks==0 and cfg.params then local the_file = table.remove(cfg.params, 1) verb_print("Param %q considered as a source file", the_file) chunks = { `File{ the_file } } end ------------------------------------------------------------------- -- If nothing to do, run REPL loop if #chunks==0 and cfg.interactive==nil then verb_print "Nothing to compile nor run, force interactive loop" cfg.interactive=true end ------------------------------------------------------------------- -- Run if asked to, or if no --output has been given -- if cfg.run==false it's been *forced* to false, don't override. if cfg.run==nil and not cfg.output then verb_print("No output file specified; I'll run the program") cfg.run = true end local code = { } ------------------------------------------------------------------- -- Get ASTs from sources local last_file for x in values(chunks) do verb_print("Compiling %s", table.tostring(x)) local st, ast match x with | `Library{ l } -> st, ast = true, `Call{ `Id 'require', `String{ l } } | `Literal{ e } -> st, ast = spring_pcall('mlc.ast_of_luastring', e, 'literal') | `File{ f } -> st, ast = spring_pcall('mlc.ast_of_luafile', f, '@'..f) -- Isolate each file in a separate fenv if st then ast = +{ function (...) -{ast} end (...) } ast.source = '@'..f -- TODO [EVE] code.source = '@'..f -- TODO [EVE] last_file = ast end end if not st then printf ("Cannot compile %s: %s", table.tostring(x), ast) os.exit (AST_COMPILE_ERROR_NUMBER) end ast.origin = x table.insert(code, ast) end -- The last file returns the whole chunk's result if last_file then local c = table.shallow_copy(last_file) last_file <- `Return{ source = c.source, c } end ------------------------------------------------------------------- -- AST printing if cfg['print-ast'] or cfg['print-ast-lineinfo'] then verb_print "Resulting AST:" for x in ivalues(code) do printf("--- AST From %s: ---", table.tostring(x.source, 'nohash')) if x.origin and x.origin.tag=='File' then x=x[1][1][2][1] end if cfg['print-ast-lineinfo'] then table.print(x, 80) else table.print(x, 80, 'nohash') end end end ------------------------------------------------------------------- -- Insert runtime loader if cfg['no-runtime'] then verb_print "Prevent insertion of command \"require 'metalua.runtime'\"" else table.insert(code, 1, +{require'metalua.runtime'}) end -- FIXME: check for failures mlc.metabugs = cfg.metabugs local bytecode = mlc.luacstring_of_ast (code) code = nil ------------------------------------------------------------------- -- Insert #!... command if cfg.sharpbang then verb_print ("Adding sharp-bang directive %q", cfg.sharpbang) if not cfg.sharpbang:strmatch'^#!' then cfg.sharpbang='#!'..cfg.sharpbang end if not cfg.sharpbang:strmatch'\n$' then cfg.sharpbang=cfg.sharpbang..'\n' end bytecode = cfg.sharpbang..bytecode end ------------------------------------------------------------------- -- Save to file if cfg.output then -- FIXME: handle '-' verb_print ("Saving to file %q", cfg.output) local file, err_msg = io.open(cfg.output, 'wb') if not file then error("can't open output file: "..err_msg) end file:write(bytecode) file:close() if cfg.sharpbang and os.getenv "OS" ~= "Windows_NT" then pcall(os.execute, 'chmod a+x "'..cfg.output..'"') end end ------------------------------------------------------------------- -- Run compiled code if cfg.run then verb_print "Running" local f = mlc.function_of_luacstring (bytecode) bytecode = nil -- FIXME: isolate execution in a ring -- FIXME: check for failures runargs = table.icat(cfg.params or { }, runargs) local st, msg = pcall(f, unpack (runargs)) if not st then io.stderr:write(msg) os.exit(RUNTIME_ERROR_NUMBER) end end ------------------------------------------------------------------- -- Run REPL loop if cfg.interactive then verb_print "Starting REPL loop" require 'metaloop' metaloop.run() -- print ("*":rep(70)) -- print "*** !!! Interactive loop not implemented !!!" -- print ("*":rep(70)) end verb_print "Done" end main(...)