]> git.lizzy.rs Git - metalua.git/blob - metalua/pprint.lua
Merge branch 'master' of ssh://git.eclipse.org/gitroot/koneki/org.eclipse.koneki...
[metalua.git] / metalua / pprint.lua
1 -------------------------------------------------------------------------------
2 -- Copyright (c) 2006-2013 Fabien Fleutot and others.
3 --
4 -- All rights reserved.
5 --
6 -- This program and the accompanying materials are made available
7 -- under the terms of the Eclipse Public License v1.0 which
8 -- accompanies this distribution, and is available at
9 -- http://www.eclipse.org/legal/epl-v10.html
10 --
11 -- This program and the accompanying materials are also made available
12 -- under the terms of the MIT public license which accompanies this
13 -- distribution, and is available at http://www.lua.org/license.html
14 --
15 -- Contributors:
16 --     Fabien Fleutot - API and implementation
17 --
18 ----------------------------------------------------------------------
19
20 ----------------------------------------------------------------------
21 ----------------------------------------------------------------------
22 --
23 -- Lua objects pretty-printer
24 --
25 ----------------------------------------------------------------------
26 ----------------------------------------------------------------------
27
28 local M = { }
29
30 M.DEFAULT_CFG = {
31     hide_hash      = false; -- Print the non-array part of tables?
32     metalua_tag    = true;  -- Use Metalua's backtick syntax sugar?
33     fix_indent     = nil;   -- If a number, number of indentation spaces;
34                             -- If false, indent to the previous brace.
35     line_max       = nil;   -- If a number, tries to avoid making lines with
36                             -- more than this number of chars.
37     initial_indent = 0;     -- If a number, starts at this level of indentation
38     keywords       = { };   -- Set of keywords which must not use Lua's field
39                             -- shortcuts {["foo"]=...} -> {foo=...}
40 }
41
42 local function valid_id(cfg, x)
43     if type(x) ~= "string" then return false end
44     if not x:match "^[a-zA-Z_][a-zA-Z0-9_]*$" then return false end
45     if cfg.keywords and cfg.keywords[x] then return false end
46     return true
47 end
48
49 local __tostring_cache = setmetatable({ }, {__mode='k'})
50
51 -- Retrieve the string produced by `__tostring` metamethod if present,
52 -- return `false` otherwise. Cached in `__tostring_cache`.
53 local function __tostring(x)
54     local the_string = __tostring_cache[x]
55     if the_string~=nil then return the_string end
56     local mt = getmetatable(x)
57     if mt then
58         local __tostring = mt.__tostring
59         if __tostring then
60             the_string = __tostring(x)
61             __tostring_cache[x] = the_string
62             return the_string
63         end
64     end
65     if x~=nil then __tostring_cache[x] = false end -- nil is an illegal key
66     return false
67 end
68
69 local xlen -- mutually recursive with `xlen_type`
70
71 local xlen_cache = setmetatable({ }, {__mode='k'})
72
73 -- Helpers for the `xlen` function
74 local xlen_type = {
75     ["nil"] = function ( ) return 3 end;
76     number  = function (x) return #tostring(x) end;
77     boolean = function (x) return x and 4 or 5 end;
78     string  = function (x) return #string.format("%q",x) end;
79 }
80
81 function xlen_type.table (adt, cfg, nested)
82     local custom_string = __tostring(adt)
83     if custom_string then return #custom_string end
84
85     -- Circular referenced objects are printed with the plain
86     -- `tostring` function in nested positions.
87     if nested [adt] then return #tostring(adt) end
88     nested [adt] = true
89
90     local has_tag  = cfg.metalua_tag and valid_id(cfg, adt.tag)
91     local alen     = #adt
92     local has_arr  = alen>0
93     local has_hash = false
94     local x = 0
95
96     if not cfg.hide_hash then
97         -- first pass: count hash-part
98         for k, v in pairs(adt) do
99             if k=="tag" and has_tag then
100                 -- this is the tag -> do nothing!
101             elseif type(k)=="number" and k<=alen and math.fmod(k,1)==0 and k>0 then
102                 -- array-part pair -> do nothing!
103             else
104                 has_hash = true
105                 if valid_id(cfg, k) then x=x+#k
106                 else x = x + xlen (k, cfg, nested) + 2 end -- count surrounding brackets
107                 x = x + xlen (v, cfg, nested) + 5          -- count " = " and ", "
108             end
109         end
110     end
111
112     for i = 1, alen do x = x + xlen (adt[i], nested) + 2 end -- count ", "
113
114     nested[adt] = false -- No more nested calls
115
116     if not (has_tag or has_arr or has_hash) then return 3 end
117     if has_tag then x=x+#adt.tag+1 end
118     if not (has_arr or has_hash) then return x end
119     if not has_hash and alen==1 and type(adt[1])~="table" then
120         return x-2 -- substract extraneous ", "
121     end
122     return x+2 -- count "{ " and " }", substract extraneous ", "
123 end
124
125
126 -- Compute the number of chars it would require to display the table
127 -- on a single line. Helps to decide whether some carriage returns are
128 -- required. Since the size of each sub-table is required many times,
129 -- it's cached in [xlen_cache].
130 xlen = function (x, cfg, nested)
131     -- no need to compute length for 1-line prints
132     if not cfg.line_max then return 0 end
133     nested = nested or { }
134     if x==nil then return #"nil" end
135     local len = xlen_cache[x]
136     if len then return len end
137     local f = xlen_type[type(x)]
138     if not f then return #tostring(x) end
139     len = f (x, cfg, nested)
140     xlen_cache[x] = len
141     return len
142 end
143
144 local function consider_newline(p, len)
145     if not p.cfg.line_max then return end
146     if p.current_offset + len <= p.cfg.line_max then return end
147     if p.indent < p.current_offset then
148         p:acc "\n"; p:acc ((" "):rep(p.indent))
149         p.current_offset = p.indent
150     end
151 end
152
153 local acc_value
154
155 local acc_type = {
156     ["nil"] = function(p) p:acc("nil") end;
157     number  = function(p, adt) p:acc (tostring (adt)) end;
158     string  = function(p, adt) p:acc ((string.format ("%q", adt):gsub("\\\n", "\\n"))) end;
159     boolean = function(p, adt) p:acc (adt and "true" or "false") end }
160
161 -- Indentation:
162 -- * if `cfg.fix_indent` is set to a number:
163 --   * add this number of space for each level of depth
164 --   * return to the line as soon as it flushes things further left
165 -- * if not, tabulate to one space after the opening brace.
166 --   * as a result, it never saves right-space to return before first element
167
168 function acc_type.table(p, adt)
169     if p.nested[adt] then p:acc(tostring(adt)); return end
170     p.nested[adt]  = true
171
172     local has_tag  = p.cfg.metalua_tag and valid_id(p.cfg, adt.tag)
173     local alen     = #adt
174     local has_arr  = alen>0
175     local has_hash = false
176
177     local previous_indent = p.indent
178
179     if has_tag then p:acc("`"); p:acc(adt.tag) end
180
181     local function indent(p)
182         if not p.cfg.fix_indent then p.indent = p.current_offset
183         else p.indent = p.indent + p.cfg.fix_indent end
184     end
185
186     -- First pass: handle hash-part
187     if not p.cfg.hide_hash then
188         for k, v in pairs(adt) do
189
190             if has_tag and k=='tag' then  -- pass the 'tag' field
191             elseif type(k)=="number" and k<=alen and k>0 and math.fmod(k,1)==0 then
192                 -- pass array-part keys (consecutive ints less than `#adt`)
193             else -- hash-part keys
194                 if has_hash then p:acc ", " else -- 1st hash-part pair ever found
195                     p:acc "{ "; indent(p)
196                 end
197
198                 -- Determine whether a newline is required
199                 local is_id, expected_len=valid_id(p.cfg, k)
200                 if is_id then expected_len=#k+xlen(v, p.cfg, p.nested)+#" = , "
201                 else expected_len = xlen(k, p.cfg, p.nested)+xlen(v, p.cfg, p.nested)+#"[] = , " end
202                 consider_newline(p, expected_len)
203
204                 -- Print the key
205                 if is_id then p:acc(k); p:acc " = " else
206                     p:acc "["; acc_value (p, k); p:acc "] = "
207                 end
208
209                 acc_value (p, v) -- Print the value
210                 has_hash = true
211             end
212         end
213     end
214
215     -- Now we know whether there's a hash-part, an array-part, and a tag.
216     -- Tag and hash-part are already printed if they're present.
217     if not has_tag and not has_hash and not has_arr then p:acc "{ }";
218     elseif has_tag and not has_hash and not has_arr then -- nothing, tag already in acc
219     else
220         assert (has_hash or has_arr) -- special case { } already handled
221         local no_brace = false
222         if has_hash and has_arr then p:acc ", "
223         elseif has_tag and not has_hash and alen==1 and type(adt[1])~="table" then
224             -- No brace required; don't print "{", remember not to print "}"
225             p:acc (" "); acc_value (p, adt[1]) -- indent= indent+(cfg.fix_indent or 0))
226             no_brace = true
227         elseif not has_hash then
228             -- Braces required, but not opened by hash-part handler yet
229             p:acc "{ "; indent(p)
230         end
231
232         -- 2nd pass: array-part
233         if not no_brace and has_arr then
234             local expected_len = xlen(adt[1], p.cfg, p.nested)
235             consider_newline(p, expected_len)
236             acc_value(p, adt[1]) -- indent+(cfg.fix_indent or 0)
237             for i=2, alen do
238                 p:acc ", ";
239                 consider_newline(p, xlen(adt[i], p.cfg, p.nested))
240                 acc_value (p, adt[i]) --indent+(cfg.fix_indent or 0)
241             end
242         end
243         if not no_brace then p:acc " }" end
244     end
245     p.nested[adt] = false -- No more nested calls
246     p.indent = previous_indent
247 end
248
249
250 function acc_value(p, v)
251     local custom_string = __tostring(v)
252     if custom_string then p:acc(custom_string) else
253         local f = acc_type[type(v)]
254         if f then f(p, v) else p:acc(tostring(v)) end
255     end
256 end
257
258
259 -- FIXME: new_indent seems to be always nil?!s detection
260 -- FIXME: accumulator function should be configurable,
261 -- so that print() doesn't need to bufferize the whole string
262 -- before starting to print.
263 function M.tostring(t, cfg)
264
265     cfg = cfg or M.DEFAULT_CFG or { }
266
267     local p = {
268         cfg = cfg;
269         indent = 0;
270         current_offset = cfg.initial_indent or 0;
271         buffer = { };
272         nested = { };
273         acc = function(self, str)
274                   table.insert(self.buffer, str)
275                   self.current_offset = self.current_offset + #str
276               end;
277     }
278     acc_value(p, t)
279     return table.concat(p.buffer)
280 end
281
282 function M.print(...) return print(M.tostring(...)) end
283 function M.sprintf(fmt, ...)
284     local args={...}
285     for i, v in pairs(args) do
286         local t=type(v)
287         if t=='table' then args[i]=M.tostring(v)
288         elseif t=='nil' then args[i]='nil' end
289     end
290     return string.format(fmt, unpack(args))
291 end
292
293 function M.printf(...) print(M.sprintf(...)) end
294
295 return M