Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
metalua/src/lib/metalua/ast_to_string.mlua
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
553 lines (499 sloc)
16.9 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -{ 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 | |