310 lines
10 KiB
Lua
310 lines
10 KiB
Lua
--[=[ The function formatting module for pretty.
|
|
|
|
How is one supposed to pretty print functions? Well, there are many different
|
|
formats, and no "best" one, only "best for the purpose". Lets first look at how
|
|
you could display the most raw data about a function.
|
|
|
|
1. The default Lua format: "function: 0x41f71c60"
|
|
This is the default, and by far the easiest. Also, very uninformative. We
|
|
only get a unique id for the function.
|
|
2. Include the arguments: "function (t, str) ... end"
|
|
This is slightly more advanced, as it requires using the debug library to
|
|
discover the names of the arguments. In addition it adds a pseudo-correct
|
|
formatting for the function. Now we know the arguments, and if they possess
|
|
descriptive names, we can learn a lot about the function.
|
|
3. Include some documentation: "function (x) --[[math.cosh: Returns the hyperbolic cosine of x.]] ... end"
|
|
We retain the arguments and pseudo-correct formatting from above, and add
|
|
documentation taken from elsewhere (for example the Lua Reference Manual, or
|
|
LuaJIT webpage), as comments. This is great for explorative programming, as
|
|
we can read about the language from within the language.
|
|
4. Short names: "math.min"
|
|
Rather than giving an overly descriptive overview of some inbuilt, we assume
|
|
that we can find it by its standard name. With this one we gain complete
|
|
native representation, assuming the standard enviroment, of course. It won't
|
|
work at all with custom functions.
|
|
5. Include source code: "function (a, b) return (a + b)/2 end"
|
|
Now we find the source code somehow, and use it as the representation. This
|
|
is powerful because we can directly inspect the code. It won't work with
|
|
builtins, closured functions, or when fucking around with the source files.
|
|
6. Include closures: "(function () local i = 5; return function () i = i + 1; return i end end)()"
|
|
In cases where a function has a closure, we can use debug.getinfo to get
|
|
the names and values of the upvalues. We can then represent the closure, by
|
|
creating the closure itself. Iterators like the example above works nicely,
|
|
but more complex chains of closures break down. For example:
|
|
|
|
local val_a = 1
|
|
local func_a = function () val_a = val_a + 1; return val_a end
|
|
local val_a = val_a
|
|
local func_c = function () return func_a() + val_a end
|
|
|
|
Here we have two functions, both with their own upvalue, both named "val_a",
|
|
yet those names refer to two different "slots". Successive calls to
|
|
`func_c` should produce the list: 2, 3, 4, 5, 6, 7, ...
|
|
To break through this barrier, we need to parse the Lua AST, and that is
|
|
beyond this project.
|
|
--]=]
|
|
|
|
-- Import
|
|
|
|
local LIBRARY
|
|
do
|
|
local thispath = ... and select('1', ...):match('.+%.') or ''
|
|
local was_loaded, library = pcall(require, thispath..'library')
|
|
LIBRARY = was_loaded and library or {}
|
|
end
|
|
|
|
-- Constants
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Util
|
|
|
|
local function get_function_info (f)
|
|
-- NOTE: Works best in LuaJIT or Lua 5.2+
|
|
-- Regarding get-info:
|
|
-- * No need to includ 'f'. Function is already known
|
|
-- * No need to include 'L' (active lines) option. Ignored
|
|
-- * No need to include 'n' (name and namewhat). Won't work.
|
|
|
|
assert(type(f) == 'function')
|
|
|
|
local info = debug.getinfo(f, 'Su')
|
|
info.params = {}
|
|
info.ups = {}
|
|
info.env = debug.getfenv and debug.getfenv(f)
|
|
info.builtin = (info.source == '=[C]')
|
|
for i = 1, info.nparams or 0 do info.params[i] = debug.getlocal(f, i) end
|
|
if info.isvararg or not info.nparams then info.params[#info.params+1] = '...' end
|
|
-- Get upvalues
|
|
for i = 1, info.nups do
|
|
local k, v = debug.getupvalue(f, i)
|
|
if k == '_ENV' and not debug.getfenv then
|
|
info.env = v
|
|
else
|
|
info.ups[k] = v
|
|
end
|
|
end
|
|
|
|
if info.source:sub(1,1) == '=' then info.defined_how = 'C'
|
|
elseif info.source:sub(1,1) == '@' then info.defined_how = 'file'
|
|
elseif info.source:find'^%w+.lua$' then info.defined_how = 'file' -- Hotfix for Love2d boot.lua issue.
|
|
else info.defined_how = 'string'
|
|
end
|
|
|
|
if info.builtin and LIBRARY[f] then
|
|
info.name = LIBRARY[f].name
|
|
info.params[1] = LIBRARY[f].para
|
|
info.doc = LIBRARY[f].docs
|
|
end
|
|
|
|
return info
|
|
end
|
|
|
|
local function get_line_index (str, line_nr)
|
|
assert(type(str) == 'string')
|
|
assert(type(line_nr) == 'number')
|
|
|
|
local index = 0
|
|
for _ = 2, line_nr do
|
|
index = str:find('\n', index, true)
|
|
if not index then return #str end
|
|
index = index + 1
|
|
end
|
|
return index
|
|
end
|
|
|
|
local function get_function_paramlist_and_body (str, start_line, end_line)
|
|
-- Will attempt to find a string which refer to a function starting on
|
|
-- line `start_line` and ending on line `end_line`.
|
|
|
|
assert(type(str) == 'string')
|
|
assert(type(start_line) == 'number')
|
|
assert(type(end_line) == 'number')
|
|
|
|
local start_line_index = get_line_index(str, start_line)
|
|
local end_line_index = get_line_index(str, end_line + 1)
|
|
|
|
local function_params, function_body = str:sub(start_line_index, end_line_index):match('function%s*[a-zA-Z0-9_.]*%s*(%([a-zA-Z0-9_,. \t]*%))[ \t]*(.-)[ \t]*end')
|
|
--print(function_params, function_body)
|
|
assert(type(function_params) == 'string' and type(function_body) == 'string')
|
|
return function_params, function_body
|
|
end
|
|
|
|
local function get_full_function_str (...)
|
|
local function_params, function_body = get_function_paramlist_and_body(...)
|
|
return 'function '..function_params..' '..function_body..' end'
|
|
end
|
|
|
|
local function get_function_str_from_file (filename, start_line, end_line)
|
|
assert(type(filename) == 'string')
|
|
assert(type(start_line) == 'number')
|
|
assert(type(end_line) == 'number')
|
|
|
|
local file = io.open(filename, 'r')
|
|
local str = file:read('*all')
|
|
file:close()
|
|
return get_full_function_str(str, start_line, end_line)
|
|
end
|
|
|
|
|
|
local function width_of_strings_in_l (l, start_i, end_i)
|
|
-- FIXME: Copy of the one in pretty.lua
|
|
local width = 0
|
|
for i = start_i or 1, (end_i or #l) do
|
|
width = width + #l[i]
|
|
end
|
|
return width
|
|
end
|
|
|
|
local function add_indent_to_string (str, indent)
|
|
assert(type(str) == 'string')
|
|
assert(type(indent) == 'string')
|
|
|
|
local l = {}
|
|
for line in string.gmatch(str, '\n', true) do
|
|
l[#l+1] = indent
|
|
l[#l+1] = line
|
|
end
|
|
return table.concat(l, '\n')
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
local function format_function_with_closure (value, depth, l, format_value)
|
|
assert(type(value) == 'function')
|
|
assert(type(depth) == 'number' and type(l) == 'table' and type(format_value) == 'function')
|
|
|
|
local info = get_function_info(value)
|
|
|
|
local function_str = nil
|
|
if (info.defined_how == 'string') then
|
|
function_str = get_full_function_str(info.source, info.linedefined, info.lastlinedefined)
|
|
else
|
|
function_str = get_function_str_from_file(info.short_src, info.linedefined, info.lastlinedefined)
|
|
end
|
|
|
|
if info.nups > 0 then l[#l+1] = '(function () ' end
|
|
-- Upvalues
|
|
for k, v in pairs(info.ups) do
|
|
l[#l+1] = 'local '
|
|
l[#l+1] = tostring(k)
|
|
l[#l+1] = ' = '
|
|
format_value(v, depth + 1, l)
|
|
l[#l+1] = '; '
|
|
end
|
|
-- Return function
|
|
if info.nups > 0 then l[#l+1] = 'return ' end
|
|
l[#l+1] = function_str
|
|
--
|
|
if info.nups > 0 then l[#l+1] = ' end)()' end
|
|
end
|
|
|
|
return function (value, depth, l, format_value)
|
|
assert(type(value) == 'function')
|
|
assert(type(depth) == 'number' and type(l) == 'table' and type(format_value) == 'function')
|
|
|
|
local info = get_function_info(value)
|
|
|
|
if l.options.include_closure and not info.builtin then
|
|
return format_function_with_closure(value, depth, l, format_value)
|
|
end
|
|
|
|
local function_params, function_body = nil, '...'
|
|
|
|
if info.defined_how == 'string' and l.options.embed_loaded_funcs then
|
|
-- Function was defined as a string.
|
|
function_params, function_body = get_function_paramlist_and_body(info.source, info.linedefined, info.lastlinedefined)
|
|
end
|
|
|
|
if info.builtin and l.options.short_builtins then
|
|
return l(info.name);
|
|
end
|
|
|
|
-- Include function modifier, and alignment info.
|
|
l[#l+1] = info.builtin and 'builtin ' or ''
|
|
l[#l+1] = { 'align', 'func_mod', #l[#l]}
|
|
|
|
-- Build rest of function signature
|
|
l[#l+1] = 'function '
|
|
local top_before = #l
|
|
if function_params then
|
|
l[#l+1] = function_params
|
|
else
|
|
l[#l+1] = '('
|
|
for _, param in ipairs(info.params) do l[#l+1], l[#l+2] = param, ', ' end
|
|
if l[#l] == ', ' then l[#l] = nil end
|
|
l[#l+1] = ')'
|
|
end
|
|
l[#l+1] = { 'align', 'func_def', width_of_strings_in_l(l, top_before) }
|
|
|
|
-- Cleanup and finish
|
|
if not l.options.more_function_info or depth ~= 0 then
|
|
l[#l+1] = (function_body:sub(1,1) == '\n') and '' or ' '
|
|
l[#l+1] = function_body
|
|
l[#l+1] = { 'align', 'func_end', #function_body }
|
|
l[#l+1] = (function_body:sub(-1) == '\n' or function_body == '') and '' or ' '
|
|
return l 'end'
|
|
end
|
|
|
|
-- More info! --
|
|
|
|
local indent = '\n' .. l.options.indent
|
|
|
|
-- Name
|
|
if info.name then
|
|
l[#l+1] = indent
|
|
l[#l+1] = '-- '
|
|
l[#l+1] = info.name
|
|
end
|
|
|
|
-- Doc
|
|
if info.doc then
|
|
for doc_line in info.doc:gmatch('[^\n]+') do
|
|
l[#l+1] = indent
|
|
l[#l+1] = '-- '
|
|
|
|
l[#l+1] = doc_line
|
|
end
|
|
end
|
|
|
|
-- source
|
|
if not info.builtin then
|
|
l[#l+1] = indent
|
|
l[#l+1] = ('-- source_file: \'%s\' '):format(info.short_src)
|
|
if info.linedefined == info.lastlinedefined then
|
|
l[#l+1] = ('[Line: %i]'):format(info.linedefined)
|
|
else
|
|
l[#l+1] = ('[Lines: %i - %i]'):format(info.linedefined, info.lastlinedefined)
|
|
end
|
|
end
|
|
|
|
-- upvalues
|
|
if info.nups > 0 and not info.builtin then
|
|
l[#l+1] = indent
|
|
l[#l+1] = '-- up_values: '
|
|
format_value(info.ups, depth + 1, l)
|
|
end
|
|
|
|
if l.options._all_function_info then
|
|
-- NOTE: This is for testing/debugging/experimentation purposes, and is
|
|
-- not designed to be pretty.
|
|
|
|
l[#l+1] = indent
|
|
l[#l+1] = '--[[ Function Body:\n\t'
|
|
l[#l+1] = add_indent_to_string(get_function_str_from_file(info.short_src, info.linedefined, info.lastlinedefined), l.options.indent)
|
|
l[#l+1] = indent
|
|
l[#l+1] = '--]]'
|
|
|
|
l[#l+1] = indent
|
|
l[#l+1] = '--[[\n\tNative repr:'
|
|
l[#l+1] = tostring(value)
|
|
l[#l+1] = indent
|
|
format_value(info, depth + 1, l)
|
|
l[#l+1] = indent
|
|
l[#l+1] = '--]]'
|
|
end
|
|
|
|
l[#l+1] = '\n'
|
|
l[#l+1] = indent
|
|
l[#l+1] = '...\nend'
|
|
end
|