380 lines
13 KiB
Lua
380 lines
13 KiB
Lua
|
|
-- pretty.function
|
|
-- The function formatting module for pretty.
|
|
|
|
--[=[ Thoughts on displaying functions in an informative way.
|
|
|
|
How is one supposed to pretty print functions? Well, there are many different
|
|
formats, and no "best" one, only "best for the purpose". Lets start at the
|
|
simplest, and move towards abstraction.
|
|
|
|
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 comes closer to a Lua
|
|
parsable formatting. We can know the argument names, and if these are
|
|
descriptive, we can learn some things about the function.
|
|
3. Include some documentation: "function (x) --[[math.cosh: Returns the hyperbolic cosine of x.]] ... end"
|
|
We retain the arguments and almost parsable formatting from above, and add
|
|
documentation taken from elsewhere - Lua Reference Manual, LuaJIT webpage -
|
|
as comments. This is great for explorative programming, as we can read about
|
|
the language by just fiddling around with it.
|
|
4. Short names: "math.min"
|
|
We can assume that an experienced Lua user might already know how any given
|
|
library function works, and would thus prefer a shorthand, rather than the
|
|
documentation. To that aim, we can use the short name/access path instead.
|
|
This is also ideal in places where we don't have a lot of space.
|
|
This representation is completely parsable, but won't necessarily work for
|
|
custom enviroment and user-defined 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. But it makes the
|
|
function representation a lot more bussy. It won't work with builtins,
|
|
closured functions, or when fucking around with the source files. It also
|
|
hits limits when functions are nested, as the debug library does not give
|
|
enough precision.
|
|
This also works nicely with 3. due to being able to read function
|
|
documentation from the source file.
|
|
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.
|
|
Closures also create extremely bussy output.
|
|
--]=]
|
|
|
|
-- 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_KEYWORD_MATCH = '%f[%a_]function%f[^%a_]'
|
|
local FUNCTION_DEFINITION_MATCH = '.-' .. -- Look for stuff before the function
|
|
FUNCTION_KEYWORD_MATCH .. '%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 LUA_FILE_PATTERN = '^%s*[%w_]+.lua%s*$'
|
|
|
|
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(LUA_FILE_PATTERN) then info.defined_how = 'file' -- Fix for when someone has misunderstood the source format is for.
|
|
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.docs = 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_docs_from_function_body (body, max_index)
|
|
-- Finds the documentation lines of a function.
|
|
-- Also returns the remaining non-documentation lines.
|
|
|
|
assert(type(body) == 'string')
|
|
|
|
local doc_lines = {}
|
|
for line in body:sub(1, max_index):gmatch('[^\n]+', true) do
|
|
if not line:match '^%s*$' then
|
|
local line_text = line:match '^%s*%-%-%s*(.*)%s*$'
|
|
doc_lines[#doc_lines+1] = line_text
|
|
if not line_text then break end
|
|
end
|
|
end
|
|
return table.concat(doc_lines, '\n'):match '^%s*(.-)%s*$'
|
|
end
|
|
|
|
local function get_docs_split_index (body)
|
|
local index = 1
|
|
while index <= #body do
|
|
local next_newline = body:find('\n', index + 1) or -1
|
|
if body:sub(index, next_newline):match('^%s*$') or body:sub(index, next_newline):match('^%s*%-%-%s*(.*)%s*$') then
|
|
index = next_newline
|
|
else
|
|
return index
|
|
end
|
|
end
|
|
return -1
|
|
end
|
|
|
|
local function get_function_body_info (info)
|
|
-- Will attempt to expand `info` with extra info found by looking at the
|
|
-- source code. This could require opening a file.
|
|
-- There is no guarentee that the function body it finds is correct, or even
|
|
-- that it finds any.
|
|
|
|
-- 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')
|
|
if file then
|
|
str = file:read '*all'
|
|
file:close()
|
|
else
|
|
str = nil
|
|
end
|
|
end
|
|
|
|
if not str then return info end
|
|
|
|
-- Now find some info about the function.
|
|
-- NOTE: function_params is currently not used for anything.
|
|
local function_name, function_params, function_body
|
|
|
|
if info.linedefined == 0 then
|
|
-- A function is a "chunk" when linedefined is equal to 0.
|
|
-- This is a "toplevel" function. One without a function
|
|
-- definition. The entire string is it's function body.
|
|
function_name = ''
|
|
function_body = str
|
|
else
|
|
-- This is not a chunk. Look for function definition.
|
|
|
|
-- 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)
|
|
|
|
function_name, function_params, function_body = str:sub(start_line_index, end_line_index):match(FUNCTION_DEFINITION_MATCH)
|
|
|
|
-- Throw an error if we can't find anything.
|
|
if 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
|
|
end
|
|
|
|
local function_body = function_body:match '^%s*(.-)%s*$'
|
|
local pivot_index = get_docs_split_index (function_body)
|
|
|
|
info.name = info.name or function_name:match '^%s*(.-)%s*$'
|
|
info.docs = info.docs or get_docs_from_function_body(function_body, pivot_index)
|
|
info.body = info.body or function_body:sub(pivot_index, -1):match '^%s*(.-)%s*$'
|
|
|
|
if info.name == '' then info.name = nil end
|
|
if info.docs == '' then info.docs = nil end
|
|
|
|
return info
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Text handling
|
|
|
|
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)
|
|
-- Indents `str` by `indent`.
|
|
|
|
assert(type(str) == 'string')
|
|
assert(type(indent) == 'string')
|
|
|
|
return indent .. str:gsub('\n', '\n'..indent)
|
|
end
|
|
|
|
local function wrap_text (text, max_width)
|
|
-- Creates a wrapped version of given `text`, with no lines longer than
|
|
-- `max_width`. Splits only at whitespace.
|
|
|
|
-- TODO: Fix this.
|
|
|
|
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
|
|
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
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
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)
|
|
|
|
local function_params, function_body = nil, '...'
|
|
|
|
if not info.docs and info.defined_how ~= 'C' and (depth == 0 or info.defined_how == 'string') then
|
|
-- Only look for documentation, when at depth 0, or when defined in
|
|
-- string. We don't want to open a ton of files constantly when
|
|
-- formatting a table.
|
|
info = get_function_body_info(info)
|
|
|
|
if info.body and #info.body <= NR_CHARS_IN_LONG_FUNCTION_BODY and not info.body:find '\n' and not info.body:find(FUNCTION_KEYWORD_MATCH) then
|
|
if info.defined_how == 'string' then function_body = info.body end
|
|
end
|
|
end
|
|
|
|
if info.builtin and depth == math.huge 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 info.docs then
|
|
l[#l+1] = '\n'
|
|
local indent = l.options.indent .. '-- '
|
|
local docs = not info.builtin and info.docs or wrap_text(info.docs, 80 - #indent)
|
|
l[#l+1] = add_indent_to_string(docs, indent)
|
|
end
|
|
|
|
-- source
|
|
if info.docs or info.name 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.docs) then
|
|
l[#l+1] = indent
|
|
l[#l+1] = '-- Up values: '
|
|
format_value(info.ups, depth + 1, l)
|
|
end
|
|
|
|
-- Ignore spacing and function body if it's a Λ string.
|
|
if function_body ~= '' then
|
|
l[#l+1] = '\n'
|
|
l[#l+1] = indent
|
|
l[#l+1] = function_body
|
|
end
|
|
l[#l+1] = '\nend'
|
|
end
|