]> git.lizzy.rs Git - metalua.git/blobdiff - metalua.lua
Merge branch 'master' of ssh://git.eclipse.org/gitroot/koneki/org.eclipse.koneki...
[metalua.git] / metalua.lua
diff --git a/metalua.lua b/metalua.lua
new file mode 100644 (file)
index 0000000..4641380
--- /dev/null
@@ -0,0 +1,274 @@
+-------------------------------------------------------------------------------
+-- Copyright (c) 2006-2013 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
+--
+-------------------------------------------------------------------------------
+
+-- 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)
+    --pp.printf("argv=%s; opts=%s, ending at %i, with optarg=%s",
+    --          argv, opts, optind, optarg)
+    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.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 }
+       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, 80, pp_cfg)
+      end
+   end
+
+   -------------------------------------------------------------------
+   -- Source printing
+   if cfg['print-src'] then
+      verb_print "Resulting sources:"
+      for _, x in ipairs(code) do
+         printf("--- Source From %s: ---", table.tostring(x.source, 'nohash'))
+         if x.origin and x.origin.tag=='File' then x=x[1][1][2] end
+         print (compiler :ast2string (x))
+      end
+   end
+
+   -- TODO: canonize/check AST
+
+   local bytecode = compiler :ast_to_bytecode (code)
+   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(...)