| #!/usr/bin/env lua51 | |
| --*-lua-*---------------------------------------------------------------------- | |
| -- | |
| -- Copyright (c) 2006-2014 Fabien Fleutot and others. | |
| -- | |
| -- All rights reserved. | |
| -- | |
| -- This program and the accompanying materials are made available | |
| -- under the terms of the Eclipse Public License v1.0 which | |
| -- accompanies this distribution, and is available at | |
| -- http://www.eclipse.org/legal/epl-v10.html | |
| -- | |
| -- This program and the accompanying materials are also made available | |
| -- under the terms of the MIT public license which accompanies this | |
| -- distribution, and is available at http://www.lua.org/license.html | |
| -- | |
| -- Contributors: | |
| -- Fabien Fleutot - API and implementation | |
| -- | |
| ------------------------------------------------------------------------------- | |
| --- | |
| -- Main executable script. | |
| -- Compiles, interprets, prints source files, runs an interactive shell, | |
| -- depending on command-line options. | |
| -- | |
| -- @module metalua | |
| -- Survive lack of checks | |
| if not pcall(require, 'checks') then function package.preload.checks() function checks() end end end | |
| -- Main file for the metalua executable | |
| require 'metalua.loader' -- load *.mlue files | |
| require 'metalua.compiler.globals' -- metalua-aware loadstring, dofile etc. | |
| local alt_getopt = require 'alt_getopt' | |
| local pp = require 'metalua.pprint' | |
| local mlc = require 'metalua.compiler' | |
| local M = { } | |
| local AST_COMPILE_ERROR_NUMBER = -1 | |
| local RUNTIME_ERROR_NUMBER = -3 | |
| local alt_getopt_options = "f:l:e:o:xivaASbs" | |
| local long_opts = { | |
| file='f', | |
| library='l', | |
| literal='e', | |
| output='o', | |
| run='x', | |
| interactive='i', | |
| verbose='v', | |
| ['print-ast']='a', | |
| ['print-ast-lineinfo']='A', | |
| ['print-src']='S', | |
| ['meta-bugs']='b', | |
| ['sharp-bang']='s', | |
| } | |
| local chunk_options = { | |
| library=1, | |
| file=1, | |
| literal=1 | |
| } | |
| local 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 are absent, 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. | |
| ]] | |
| function M.cmdline_parser(...) | |
| local argv = {...} | |
| local opts, optind, optarg = | |
| alt_getopt.get_ordered_opts({...}, alt_getopt_options, long_opts) | |
| local s2l = { } -- short to long option names conversion table | |
| for long, short in pairs(long_opts) do s2l[short]=long end | |
| local cfg = { chunks = { } } | |
| for i, short in pairs(opts) do | |
| local long = s2l[short] | |
| if chunk_options[long] then table.insert(cfg.chunks, { tag=long, optarg[i] }) | |
| else cfg[long] = optarg[i] or true end | |
| end | |
| cfg.params = { select(optind, ...) } | |
| return cfg | |
| end | |
| function M.main (...) | |
| local cfg = M.cmdline_parser(...) | |
| ------------------------------------------------------------------- | |
| -- Print messages if in verbose mode | |
| local function verb_print (fmt, ...) | |
| if cfg.verbose then | |
| return pp.printf ("[ "..fmt.." ]", ...) | |
| end | |
| end | |
| if cfg.verbose then | |
| verb_print("raw options: %s", cfg) | |
| end | |
| ------------------------------------------------------------------- | |
| -- If there's no chunk but there are params, interpret the first | |
| -- param as a file name. | |
| if not next(cfg.chunks) and next(cfg.params) then | |
| local the_file = table.remove(cfg.params, 1) | |
| verb_print("Param %q considered as a source file", the_file) | |
| cfg.chunks={ { tag='file', the_file } } | |
| end | |
| ------------------------------------------------------------------- | |
| -- If nothing to do, run REPL loop | |
| if not next(cfg.chunks) and not cfg.interactive 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 not cfg.run 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_idx | |
| for i, x in ipairs(cfg.chunks) do | |
| local compiler = mlc.new() | |
| local tag, val = x.tag, x[1] | |
| verb_print("Compiling %s", x) | |
| local st, ast | |
| if tag=='library' then | |
| ast = { tag='Call', | |
| {tag='Id', "require" }, | |
| {tag='String', val } } | |
| elseif tag=='literal' then ast = compiler :src_to_ast(val) | |
| elseif tag=='file' then | |
| ast = compiler :srcfile_to_ast(val) | |
| -- Isolate each file in a separate fenv | |
| ast = { tag='Call', | |
| { tag='Function', { { tag='Dots'} }, ast }, | |
| { tag='Dots' } } | |
| ast.source = '@'..val | |
| code.source = '@'..val | |
| last_file_idx = i | |
| else | |
| error ("Bad option "..tag) | |
| end | |
| local valid = true -- TODO: check AST's correctness | |
| if not valid then | |
| pp.printf ("Cannot compile %s:\n%s", x, ast or "no msg") | |
| 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_idx then | |
| -- transform `+{ (function(...) -{ast} end)(...) }` | |
| -- into `+{ return (function(...) -{ast} end)(...) }` | |
| local prv_ast = code[last_file_idx] | |
| local new_ast = { tag='Return', prv_ast } | |
| new_ast.source, new_ast.origin, prv_ast.source, prv_ast.origin = | |
| prv_ast.source, prv_ast.origin, nil, nil | |
| code[last_file_idx] = new_ast | |
| end | |
| -- Further uses of compiler won't involve AST transformations: | |
| -- they can share the same instance. | |
| -- TODO: reuse last instance if possible. | |
| local compiler = mlc.new() | |
| ------------------------------------------------------------------- | |
| -- AST printing | |
| if cfg['print-ast'] or cfg['print-ast-lineinfo'] then | |
| verb_print "Resulting AST:" | |
| for _, x in ipairs(code) do | |
| pp.printf("--- AST From %s: ---", x.source) | |
| if x.origin and x.origin.tag=='File' then x=x[1][1][2][1] end | |
| local pp_cfg = cfg['print-ast-lineinfo'] | |
| and { line_max=1, fix_indent=1, metalua_tag=1 } | |
| or { line_max=1, metalua_tag=1, hide_hash=1 } | |
| pp.print(x, pp_cfg) | |
| end | |
| end | |
| ------------------------------------------------------------------- | |
| -- Source printing | |
| if cfg['print-src'] then | |
| verb_print "Resulting sources:" | |
| for _, x in ipairs(code) do | |
| pp.printf("--- Source From %s: ---", pp.tostring(x.source, 'nohash')) | |
| if x.origin and x.origin.tag=='File' then x=x[1][1][2] end | |
| print (compiler :ast_to_src(x)) | |
| end | |
| end | |
| -- TODO: canonize/check AST | |
| local bytecode = compiler :ast_to_bytecode (code, code.source) | |
| code = nil | |
| ------------------------------------------------------------------- | |
| -- Insert #!... command | |
| if cfg.sharpbang then | |
| local shbang = cfg.sharpbang | |
| verb_print ("Adding sharp-bang directive %q", shbang) | |
| if not shbang :match'^#!' then shbang = '#!' .. shbang end | |
| if not shbang :match'\n$' then shbang = shbang .. '\n' end | |
| bytecode = shbang .. 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 = compiler :bytecode_to_function (bytecode) | |
| bytecode = nil | |
| -- FIXME: isolate execution in a ring | |
| -- FIXME: check for failures | |
| local function print_traceback (errmsg) | |
| return errmsg .. '\n' .. debug.traceback ('',2) .. '\n' | |
| end | |
| local function g() return f(unpack (cfg.params)) end | |
| local st, msg = xpcall(g, print_traceback) | |
| 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 'metalua.repl' .run() | |
| end | |
| verb_print "Done" | |
| end | |
| return M.main(...) |