369 lines
12 KiB
Lua
369 lines
12 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
|
|
|
|
-- FUNCTION_DEFINITION_MATCH is a lua pattern, for finding a function definition.
|
|
-- NOTE: It will match malformed unicode sequences, and thus assumes that the
|
|
-- string checked against have been checked by the lua interpreter.
|
|
local FUNCTION_DEFINITION_MATCH =
|
|
'.*%f[%a_]function%f[^%a_]%s*' .. -- Look for the function keyword
|
|
'([a-zA-Z0-9\128-\255_.:]*)' .. -- Look for the function name, if any
|
|
'%s*(%([a-zA-Z0-9\128-\255_,. \t]*%))' .. -- Look for the function parameter list.
|
|
'[ \t]*(.+)[ \t]*' .. -- Look for the function body
|
|
'end' -- Look for the end keyword
|
|
|
|
local NR_CHARS_IN_LONG_FUNCTION_BODY = 30
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- 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 (info)
|
|
-- Will attempt to find a string which refer to the function. This will
|
|
-- possibly require opening a file.
|
|
|
|
-- Error check
|
|
assert(type(info) == 'table')
|
|
|
|
if info.defined_how == 'C' then
|
|
error('[pretty.function/internal]: Cannot find source-code for C functions.', 2)
|
|
end
|
|
|
|
-- First find the string to search through.
|
|
local str = info.source
|
|
if info.defined_how == 'file' then
|
|
-- Read file
|
|
local file = io.open(info.short_src, 'r')
|
|
str = file:read '*all'
|
|
file:close()
|
|
end
|
|
|
|
-- Calculate indices of the lines the function should be defined at.
|
|
local start_line_index = get_line_index(str, info.linedefined)
|
|
local end_line_index = get_line_index(str, info.lastlinedefined + 1)
|
|
|
|
-- Now find the function parameters and the function body.
|
|
local function_name, function_params, function_body = str:sub(start_line_index, end_line_index):match(FUNCTION_DEFINITION_MATCH)
|
|
-- TODO: Use function_name for something.
|
|
if type(function_params) ~= 'string' or type(function_body) ~= 'string' then
|
|
error(('[pretty.function/internal]: Could not find the function defined on lines %i-%i (indices %i-%i) for string:\n\n%s\n'):format(info.linedefined, info.lastlinedefined, start_line_index, end_line_index, str))
|
|
end
|
|
function_body = function_body:match('^%s*(.-)%s*$')
|
|
assert(type(function_body) == 'string')
|
|
-- And return them.
|
|
return function_params, function_body
|
|
end
|
|
|
|
local function get_function_string (...)
|
|
return string.format('function %s %s end', get_function_paramlist_and_body(...))
|
|
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')
|
|
|
|
return indent .. str:gsub('\n', '\n'..indent)
|
|
end
|
|
|
|
local function get_docs_from_function_body (func_body)
|
|
assert(type(func_body) == 'string')
|
|
local doc_lines = {}
|
|
for line in func_body:gmatch('[^\n]+', true) do
|
|
if not line:match('^%s*$') then
|
|
local line_text = line:match('^%s*%-%-%s*(.*)%s*$')
|
|
if not line_text then break end
|
|
doc_lines[#doc_lines+1] = line_text
|
|
end
|
|
end
|
|
return table.concat(doc_lines, '\n')
|
|
end
|
|
|
|
local function wrap_text (text, max_width)
|
|
local l, i, last_i = {}, max_width, 1
|
|
repeat
|
|
if text:sub(i, i) == ' ' then
|
|
l[#l+1], last_i, i = text:sub(last_i, i - 1), i + 1, i + max_width
|
|
elseif i <= last_i then
|
|
-- TODO: Make sure this part works.
|
|
i = text:find(' ', last_i) or #text
|
|
else
|
|
i = i - 1
|
|
end
|
|
until i >= #text
|
|
l[#l+1] = text:sub(last_i)
|
|
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 = get_function_string(info)
|
|
|
|
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' then
|
|
-- Function was defined as a string
|
|
local _, body = get_function_paramlist_and_body(info)
|
|
if #body <= NR_CHARS_IN_LONG_FUNCTION_BODY and not body:find '\n' then
|
|
function_body = body
|
|
end
|
|
end
|
|
|
|
if info.builtin and l.options.short_builtins then
|
|
assert(info.name)
|
|
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 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 not info.doc then
|
|
local function_body = select(2, get_function_paramlist_and_body(info))
|
|
if function_body then
|
|
local documentation = get_docs_from_function_body(function_body)
|
|
info.doc = documentation ~= '' and documentation
|
|
end
|
|
end
|
|
|
|
if info.doc then
|
|
l[#l+1] = '\n'
|
|
local indent = l.options.indent .. '-- '
|
|
local docs = not info.builtin and info.doc or wrap_text(info.doc, 80 - #indent)
|
|
l[#l+1] = add_indent_to_string(docs, indent)
|
|
end
|
|
|
|
-- source
|
|
if info.doc then -- Do nothing
|
|
elseif info.defined_how == 'string' then
|
|
l[#l+1] = indent
|
|
l[#l+1] = '-- Loaded from string'
|
|
elseif 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 and not info.doc) 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.
|
|
|
|
-- Native
|
|
l[#l+1] = indent
|
|
l[#l+1] = '-- Native Representation: '
|
|
l[#l+1] = tostring(value)
|
|
|
|
-- Function body
|
|
if info.defined_how ~= 'C' then
|
|
l[#l+1] = indent
|
|
l[#l+1] = '--[[ Function Body:\n\t'
|
|
l[#l+1] = add_indent_to_string(get_function_string(info), l.options.indent)
|
|
l[#l+1] = indent
|
|
l[#l+1] = '--]]'
|
|
end
|
|
|
|
-- Full info
|
|
l[#l+1] = indent
|
|
l[#l+1] = '--[[ full_info:\n'
|
|
info.env = nil
|
|
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
|