1
0
pretty/function.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_.:]*)%s*' .. -- Look for the function name, if any
'(%([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