]> git.lizzy.rs Git - metalua.git/commitdiff
support for ast->src translation in lib and metalua executable
authorFabien Fleutot <fabien@MacFabien.home>
Thu, 29 Jan 2009 20:48:49 +0000 (21:48 +0100)
committerFabien Fleutot <fabien@MacFabien.home>
Thu, 29 Jan 2009 20:48:49 +0000 (21:48 +0100)
src/compiler/metalua.mlua
src/lib/metalua/ast_to_string.mlua [new file with mode: 0644]

index 8fe2c78e017d3bc310764171bc43479ebb9c228e..0620638f7e02490b1082dbb135412e56f2a3f1aa 100644 (file)
@@ -67,6 +67,9 @@ parser = clopts {
    {  short = 'A', long = 'print-ast-lineinfo',  type = 'boolean',
       usage = 'print the AST resulting from file compilation, including lineinfo data'
    },
+   {  short = 'S', long = 'print-src',  type = 'boolean',
+      usage = 'print the AST resulting from file compilation, as re-gerenerated sources'
+   },
    {  short = 'b', long = 'metabugs', type = 'boolean',
       usage = 'show syntax errors as compile-time execution errors'
    },
@@ -189,6 +192,18 @@ local function main (...)
       end
    end
 
+   -------------------------------------------------------------------
+   -- Source printing
+   if cfg['print-src'] then
+      verb_print "Resulting sources:"
+      require 'metalua.ast_to_string'
+      for x in ivalues(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][1] end
+         print (ast_to_string (x))
+      end
+   end
+
    -------------------------------------------------------------------
    -- Insert runtime loader
    if cfg['no-runtime'] then
diff --git a/src/lib/metalua/ast_to_string.mlua b/src/lib/metalua/ast_to_string.mlua
new file mode 100644 (file)
index 0000000..e27827d
--- /dev/null
@@ -0,0 +1,553 @@
+-{ extension 'match' }
+
+local M = { }
+M.__index = M
+
+ast_to_string = |x| M.run(x)
+
+--------------------------------------------------------------------------------
+-- Instanciate a new AST->source synthetizer
+--------------------------------------------------------------------------------
+function M.new ()
+   local self = {
+      _acc           = { },  -- Accumulates pieces of source as strings
+      current_indent = 0,    -- Current level of line indentation
+      indent_step    = "   " -- Indentation symbol, normally spaces or '\t'
+   }
+   return setmetatable (self, M)
+end
+
+--------------------------------------------------------------------------------
+-- Run a synthetizer on the `ast' arg and return the source as a string.
+-- Can also be used as a static method `M.run (ast)'; in this case,
+-- a temporary Metizer is instanciated on the fly.
+--------------------------------------------------------------------------------
+function M:run (ast)
+   if not ast then
+      self, ast = M.new(), self
+   end
+   self._acc = { }
+   self:node (ast)
+   return table.concat (self._acc)
+end
+
+--------------------------------------------------------------------------------
+-- Accumulate a piece of source file in the synthetizer.
+--------------------------------------------------------------------------------
+function M:acc (x)
+   if x then table.insert (self._acc, x) end
+end
+
+--------------------------------------------------------------------------------
+-- Accumulate an indented newline.
+-- Jumps an extra line if indentation is 0, so that
+-- toplevel definitions are separated by an extra empty line.
+--------------------------------------------------------------------------------
+function M:nl ()
+   if self.current_indent == 0 then self:acc "\n"  end
+   self:acc ("\n" .. self.indent_step:rep (self.current_indent))
+end
+
+--------------------------------------------------------------------------------
+-- Increase indentation and accumulate a new line.
+--------------------------------------------------------------------------------
+function M:nlindent ()
+   self.current_indent = self.current_indent + 1
+   self:nl ()
+end
+
+--------------------------------------------------------------------------------
+-- Decrease indentation and accumulate a new line.
+--------------------------------------------------------------------------------
+function M:nldedent ()
+   self.current_indent = self.current_indent - 1
+   self:acc ("\n" .. self.indent_step:rep (self.current_indent))
+end
+
+--------------------------------------------------------------------------------
+-- Keywords, which are illegal as identifiers.
+--------------------------------------------------------------------------------
+local keywords = table.transpose {
+   "and",    "break",   "do",    "else",   "elseif",
+   "end",    "false",   "for",   "function", "if",
+   "in",     "local",   "nil",   "not",    "or",
+   "repeat", "return",  "then",  "true",   "until",
+   "while" }
+
+--------------------------------------------------------------------------------
+-- Return true iff string `id' is a legal identifier name.
+--------------------------------------------------------------------------------
+local function is_ident (id)
+   return id:strmatch "^[%a_][%w_]*$" and not keywords[id]
+end
+
+--------------------------------------------------------------------------------
+-- Return true iff ast represents a legal function name for
+-- syntax sugar ``function foo.bar.gnat() ... end'':
+-- a series of nested string indexes, with an identifier as
+-- the innermost node.
+--------------------------------------------------------------------------------
+local function is_idx_stack (ast)
+   match ast with
+   | `Id{ _ }                     -> return true
+   | `Index{ left, `String{ _ } } -> return is_idx_stack (left)
+   | _                            -> return false
+   end
+end
+
+--------------------------------------------------------------------------------
+-- Operator precedences, in increasing order.
+-- This is not directly used, it's used to generate op_prec below.
+--------------------------------------------------------------------------------
+local op_preprec = {
+   { "or", "and" },
+   { "lt", "le", "eq", "ne" },
+   { "concat" }, 
+   { "add", "sub" },
+   { "mul", "div", "mod" },
+   { "unary", "not", "len" },
+   { "pow" },
+   { "index" } }
+
+--------------------------------------------------------------------------------
+-- operator --> precedence table, generated from op_preprec.
+--------------------------------------------------------------------------------
+local op_prec = { }
+
+for prec, ops in ipairs (op_preprec) do
+   for op in ivalues (ops) do
+      op_prec[op] = prec
+   end
+end
+
+--------------------------------------------------------------------------------
+-- operator --> source representation.
+--------------------------------------------------------------------------------
+local op_symbol = {
+   add    = " + ",   sub     = " - ",   mul     = " * ",
+   div    = " / ",   mod     = " % ",   pow     = " ^ ",
+   concat = " .. ",  eq      = " == ",  ne      = " ~= ",
+   lt     = " < ",   le      = " <= ",  ["and"] = " and ",
+   ["or"] = " or ",  ["not"] = "not ",  len     = "# " }
+
+--------------------------------------------------------------------------------
+-- Accumulate the source representation of AST `node' in
+-- the synthetizer. Most of the work is done by delegating to
+-- the method having the name of the AST tag.
+-- If something can't be converted to normal sources, it's
+-- instead dumped as a `-{ ... }' splice in the source accumulator.
+--------------------------------------------------------------------------------
+function M:node (node)
+   assert (self~=M and self._acc)
+   if not node.tag then -- tagless block.
+      self:list (node, self.nl)
+   else
+      local f = M[node.tag]
+      if type (f) == "function" then -- Delegate to tag method.
+         f (self, node, unpack (node))
+      elseif type (f) == "string" then -- tag string.
+         self:acc (f)
+      else -- No appropriate method, fall back to splice dumping.
+           -- This cannot happen in a plain Lua AST.
+         self:acc " -{ "
+         self:acc (table.tostring (node, "nohash"), 80)
+         self:acc " }"
+      end
+   end
+end
+
+--------------------------------------------------------------------------------
+-- Convert every node in the AST list `list' passed as 1st arg.
+-- `sep' is an optional separator to be accumulated between each list element,
+-- it can be a string or a synth method.
+-- `start' is an optional number (default == 1), indicating which is the
+-- first element of list to be converted, so that we can skip the begining
+-- of a list. 
+--------------------------------------------------------------------------------
+function M:list (list, sep, start)
+   for i = start or 1, # list do
+      self:node (list[i])
+      if list[i + 1] then
+         if not sep then            
+         elseif type (sep) == "function" then sep (self)
+         elseif type (sep) == "string"   then self:acc (sep)
+         else   error "Invalid list separator" end
+      end
+   end
+end
+
+--------------------------------------------------------------------------------
+--
+-- Tag methods.
+-- ------------
+--
+-- Specific AST node dumping methods, associated to their node kinds
+-- by their name, which is the corresponding AST tag.
+-- synth:node() is in charge of delegating a node's treatment to the
+-- appropriate tag method.
+--
+-- Such tag methods are called with the AST node as 1st arg.
+-- As a convenience, the n node's children are passed as args #2 ... n+1.
+--
+-- There are several things that could be refactored into common subroutines
+-- here: statement blocks dumping, function dumping...
+-- However, given their small size and linear execution
+-- (they basically perform series of :acc(), :node(), :list(), 
+-- :nl(), :nlindent() and :nldedent() calls), it seems more readable
+-- to avoid multiplication of such tiny functions.
+--
+-- To make sense out of these, you need to know metalua's AST syntax, as
+-- found in the reference manual or in metalua/doc/ast.txt.
+--
+--------------------------------------------------------------------------------
+
+function M:Do (node)
+   self:acc      "do"
+   self:nlindent ()
+   self:list     (node, self.nl)
+   self:nldedent ()
+   self:acc      "end"
+end
+
+function M:Set (node)
+   match node with
+   | `Set{ { `Index{ lhs, `String{ method } } }, 
+           { `Function{ { `Id "self", ... } == params, body } } } 
+         if is_idx_stack (lhs) and is_ident (method) ->
+      -- ``function foo:bar(...) ... end'' --
+      self:acc      "function "
+      self:node     (lhs)
+      self:acc      ":"
+      self:acc      (method)
+      self:acc      " ("
+      self:list     (params, ", ", 2)
+      self:acc      ")"
+      self:nlindent ()
+      self:list     (body, self.nl)
+      self:nldedent ()
+      self:acc      "end"
+
+   | `Set{ { lhs }, { `Function{ params, body } } } if is_idx_stack (lhs) ->
+      -- ``function foo(...) ... end'' --
+      self:acc      "function "
+      self:node     (lhs)
+      self:acc      " ("
+      self:list     (params, ", ")
+      self:acc      ")"
+      self:nlindent ()
+      self:list    (body, self.nl)
+      self:nldedent ()
+      self:acc      "end"
+
+   | `Set{ { `Id{ lhs1name } == lhs1, ... } == lhs, rhs } 
+         if not is_ident (lhs1name) ->
+      -- ``foo, ... = ...'' when foo is *not* a valid identifier.
+      -- In that case, the spliced 1st variable must get parentheses,
+      -- to be distinguished from a statement splice.
+      -- This cannot happen in a plain Lua AST.
+      self:acc      "("
+      self:node     (lhs1)
+      self:acc      ")"
+      if lhs[2] then -- more than one lhs variable
+         self:acc   ", "
+         self:list  (lhs, ", ", 2)
+      end
+      self:acc      " = "
+      self:list     (rhs, ", ")
+
+   | `Set{ lhs, rhs } ->
+      -- ``... = ...'', no syntax sugar --
+      self:list  (lhs, ", ")
+      self:acc   " = "
+      self:list  (rhs, ", ")
+   end
+end
+
+function M:While (node, cond, body)
+   self:acc      "while "
+   self:node     (cond)
+   self:acc      " do"
+   self:nlindent ()
+   self:list     (body, self.nl)
+   self:nldedent ()
+   self:acc      "end"
+end
+
+function M:Repeat (node, body, cond)
+   self:acc      "repeat"
+   self:nlindent ()
+   self:list     (body, self.nl)
+   self:nldedent ()
+   self:acc      "until "
+   self:node     (cond)
+end
+
+function M:If (node)
+   for i = 1, #node-1, 2 do
+      -- for each ``if/then'' and ``elseif/then'' pair --
+      local cond, body = node[i], node[i+1]
+      self:acc      (i==1 and "if " or "elseif ")
+      self:node     (cond)
+      self:acc      " then"
+      self:nlindent ()
+      self:list     (body, self.nl)
+      self:nldedent ()
+   end
+   -- odd number of children --> last one is an `else' clause --
+   if #node%2 == 1 then 
+      self:acc      "else"
+      self:nlindent ()
+      self:list     (node[#node], self.nl)
+      self:nldedent ()
+   end
+   self:acc "end"
+end
+
+function M:Fornum (node, var, first, last)
+   local body = node[#node]
+   self:acc      "for "
+   self:node     (var)
+   self:acc      " = "
+   self:node     (first)
+   self:acc      ", "
+   self:node     (last)
+   if #node==5 then -- 5 children --> child #4 is a step increment.
+      self:acc   ", "
+      self:node  (node[4])
+   end
+   self:acc      " do"
+   self:nlindent ()
+   self:list     (body, self.nl)
+   self:nldedent ()
+   self:acc      "end"
+end
+
+function M:Forin (node, vars, generators, body)
+   self:acc      "for "
+   self:list     (vars, ", ")
+   self:acc      " in "
+   self:list     (generators, ", ")
+   self:acc      " do"
+   self:nlindent ()
+   self:list     (body, self.nl)
+   self:nldedent ()
+   self:acc      "end"
+end
+
+function M:Local (node, lhs, rhs)
+   if next (lhs) then
+      self:acc     "local "
+      self:list    (lhs, ", ")
+      if rhs[1] then
+         self:acc  " = "
+         self:list (rhs, ", ")
+      end
+   else -- Can't create a local statement with 0 variables in plain Lua
+      self:acc (table.tostring (node, 'nohash', 80))
+   end
+end
+
+function M:Localrec (node, lhs, rhs)
+   match node with
+   | `Localrec{ { `Id{name} }, { `Function{ params, body } } }
+         if is_ident (name) ->
+      -- ``local function name() ... end'' --
+      self:acc      "local function "
+      self:acc      (name)
+      self:acc      " ("
+      self:list     (params, ", ")
+      self:acc      ")"
+      self:nlindent ()
+      self:list     (body, self.nl)
+      self:nldedent ()
+      self:acc      "end"
+
+   | _ -> 
+      -- Other localrec are unprintable ==> splice them --
+          -- This cannot happen in a plain Lua AST. --
+      self:acc "-{ "
+      self:acc (table.tostring (node, 'nohash', 80))
+      self:acc " }"
+   end
+end
+
+function M:Call (node, f)
+   -- single string or table literal arg ==> no need for parentheses. --
+   local parens
+   match node with
+   | `Call{ _, `String{_} }
+   | `Call{ _, `Table{...}} -> parens = false
+   | _ -> parens = true
+   end
+   self:node (f)
+   self:acc  (parens and " (" or  " ")
+   self:list (node, ", ", 2) -- skip `f'.
+   self:acc  (parens and ")")
+end
+
+function M:Invoke (node, f, method)
+   -- single string or table literal arg ==> no need for parentheses. --
+   local parens
+   match node with
+   | `Invoke{ _, _, `String{_} }
+   | `Invoke{ _, _, `Table{...}} -> parens = false
+   | _ -> parens = true
+   end
+   self:node   (f)
+   self:acc    ":"
+   self:acc    (method[1])
+   self:acc    (parens and " (" or  " ")
+   self:list   (node, ", ", 3) -- Skip args #1 and #2, object and method name.
+   self:acc    (parens and ")")
+end
+
+function M:Return (node)
+   self:acc  "return "
+   self:list (node, ", ")
+end
+
+M.Break = "break"
+M.Nil   = "nil"
+M.False = "false"
+M.True  = "true"
+M.Dots  = "..."
+
+function M:Number (node, n)
+   self:acc (tostring (n))
+end
+
+function M:String (node, str)
+   -- format "%q" prints '\n' in an umpractical way IMO,
+   -- so this is fixed with the :gsub( ) call.
+   self:acc (string.format ("%q", str):gsub ("\\\n", "\\n"))
+end
+
+function M:Function (node, params, body)
+   self:acc      "function ("
+   self:list     (params, ", ")
+   self:acc      ")"
+   self:nlindent ()
+   self:list     (body, self.nl)
+   self:nldedent ()
+   self:acc      "end"
+end
+
+function M:Table (node)
+   if not node[1] then self:acc "{ }" else
+      self:acc "{"
+      if #node > 1 then self:nlindent () else self:acc " " end
+      for i, elem in ipairs (node) do
+         match elem with
+         | `Pair{ `String{ key }, value } if is_ident (key) ->
+            -- ``key = value''. --
+            self:acc  (key)
+            self:acc  " = "
+            self:node (value)
+
+         | `Pair{ key, value } ->
+            -- ``[key] = value''. --
+            self:acc  "["
+            self:node (key)
+            self:acc  "] = "
+            self:node (value)
+
+         | _ -> 
+            -- ``value''. --
+            self:node (elem)
+         end
+         if node [i+1] then
+            self:acc ","
+            self:nl  ()
+         end
+      end
+      if #node > 1 then self:nldedent () else self:acc " " end
+      self:acc       "}"
+   end
+end
+
+function M:Op (node, op, a, b)
+   -- Transform ``not (a == b)'' into ``a ~= b''. --
+   match node with
+   | `Op{ "not", `Op{ "eq", _a, _b } }
+   | `Op{ "not", `Paren{ `Op{ "eq", _a, _b } } } ->  
+      op, a, b = "ne", _a, _b
+   | _ ->
+   end
+
+   if b then -- binary operator.
+      local left_paren, right_paren
+      match a with
+      | `Op{ op_a, ...} if op_prec[op] >= op_prec[op_a] -> left_paren = true
+      | _ -> left_paren = false
+      end
+
+      match b with -- FIXME: might not work with right assoc operators ^ and ..
+      | `Op{ op_b, ...} if op_prec[op] >= op_prec[op_b] -> right_paren = true
+      | _ -> right_paren = false
+      end
+
+      self:acc  (left_paren and "(")
+      self:node (a)
+      self:acc  (left_paren and ")")
+
+      self:acc  (op_symbol [op])
+
+      self:acc  (right_paren and "(")
+      self:node (b)
+      self:acc  (right_paren and ")")
+
+   else -- unary operator.     
+      local paren
+      match a with
+      | `Op{ op_a, ... } if op_prec[op] >= op_prec[op_a] -> paren = true
+      | _ -> paren = false
+      end
+      self:acc  (op_symbol[op])
+      self:acc  (paren and "(")
+      self:node (a)
+      self:acc  (paren and ")")
+   end
+end
+
+function M:Paren (node, content)
+   self:acc  "("
+   self:node (content)
+   self:acc  ")"
+end
+
+function M:Index (node, table, key)
+   local paren_table
+   -- Check precedence, see if parens are needed around the table --
+   match table with
+   | `Op{ op, ... } if op_prec[op] < op_prec.index -> paren_table = true
+   | _ -> paren_table = false
+   end
+
+   self:acc  (paren_table and "(")
+   self:node (table)
+   self:acc  (paren_table and ")")
+
+   match key with
+   | `String{ field } if is_ident (field) -> 
+      -- ``table.key''. --
+      self:acc "."
+      self:acc (field)
+   | _ -> 
+      -- ``table [key]''. --
+      self:acc   "["
+      self:node (key)
+      self:acc   "]"
+   end
+end
+
+function M:Id (node, name)
+   if is_ident (name) then
+      self:acc (name)
+   else -- Unprintable identifier, fall back to splice representation.
+        -- This cannot happen in a plain Lua AST.
+      self:acc    "-{`Id "
+      self:String (node, name)
+      self:acc    "}"
+   end 
+end
+