2017-07-17 19:33:11 +00:00
-- pretty.function
-- The function formatting module for pretty.
--[=[ Thoughts on displaying functions in an informative way.
2017-04-06 12:46:45 +00:00
How is one supposed to pretty print functions ? Well , there are many different
2017-07-15 21:11:54 +00:00
formats , and no " best " one , only " best for the purpose " . Lets start at the
simplest , and move towards abstraction .
2017-04-06 12:46:45 +00:00
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
2017-07-15 21:11:54 +00:00
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 .
2017-04-06 12:46:45 +00:00
3. Include some documentation : " function (x) --[[math.cosh: Returns the hyperbolic cosine of x.]] ... end "
2017-07-15 21:11:54 +00:00
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 .
2017-04-06 12:46:45 +00:00
4. Short names : " math.min "
2017-07-15 21:11:54 +00:00
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 .
2017-04-06 12:46:45 +00:00
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
2017-07-15 21:11:54 +00:00
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 .
2017-04-06 12:46:45 +00:00
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 .
2017-07-15 21:11:54 +00:00
Closures also create extremely bussy output .
2017-04-06 12:46:45 +00:00
--]=]
2017-04-03 09:55:49 +00:00
-- 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
2017-07-15 18:43:25 +00:00
-- 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.
2017-07-15 19:25:32 +00:00
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
2017-07-15 18:59:55 +00:00
' ([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
2017-07-15 19:25:32 +00:00
' end ' -- Look for the end keyword
2017-07-15 18:43:25 +00:00
2017-07-15 18:10:49 +00:00
local NR_CHARS_IN_LONG_FUNCTION_BODY = 30
2017-04-03 09:55:49 +00:00
--------------------------------------------------------------------------------
-- Util
local function get_function_info ( f )
-- NOTE: Works best in LuaJIT or Lua 5.2+
2017-06-05 21:24:24 +00:00
-- Regarding get-info:
2017-04-03 09:55:49 +00:00
-- * 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.
2017-06-05 21:24:24 +00:00
assert ( type ( f ) == ' function ' )
2017-04-03 09:55:49 +00:00
local info = debug.getinfo ( f , ' Su ' )
info.params = { }
info.ups = { }
info.env = debug.getfenv and debug.getfenv ( f )
2017-04-03 11:56:06 +00:00
info.builtin = ( info.source == ' =[C] ' )
2017-04-03 09:55:49 +00:00
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 '
2017-06-11 11:53:06 +00:00
elseif info.source : sub ( 1 , 1 ) == ' @ ' then info.defined_how = ' file '
2017-07-22 12:44:24 +00:00
elseif info.source : find ' ^%w+.lua$ ' then info.defined_how = ' file ' -- Hotfix for Love2d boot.lua issue.
2017-04-03 09:55:49 +00:00
else info.defined_how = ' string '
end
if info.builtin and LIBRARY [ f ] then
info.name = LIBRARY [ f ] . name
info.params [ 1 ] = LIBRARY [ f ] . para
2017-07-15 20:56:10 +00:00
info.docs = LIBRARY [ f ] . docs
2017-04-03 09:55:49 +00:00
end
return info
end
local function get_line_index ( str , line_nr )
2017-06-11 11:53:06 +00:00
assert ( type ( str ) == ' string ' )
assert ( type ( line_nr ) == ' number ' )
2017-04-03 09:55:49 +00:00
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
2017-07-15 20:56:10 +00:00
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.
2017-06-11 11:53:06 +00:00
2017-07-14 14:51:19 +00:00
-- 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
2017-06-11 11:53:06 +00:00
2017-07-14 14:51:19 +00:00
-- 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 ' )
2017-07-15 21:29:51 +00:00
if file then
str = file : read ' *all '
file : close ( )
else
str = nil
end
2017-07-14 14:51:19 +00:00
end
2017-07-15 21:29:51 +00:00
if not str then return info end
2017-07-14 14:51:19 +00:00
-- 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 )
2017-04-03 09:55:49 +00:00
2017-07-15 20:56:10 +00:00
-- Now find some info about the function.
-- NOTE: function_params is currently not used for anything.
2017-07-15 18:43:25 +00:00
local function_name , function_params , function_body = str : sub ( start_line_index , end_line_index ) : match ( FUNCTION_DEFINITION_MATCH )
2017-07-15 20:56:10 +00:00
if type ( function_body ) ~= ' string ' then
2017-07-15 18:43:25 +00:00
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
2017-06-11 11:53:06 +00:00
2017-07-15 20:56:10 +00:00
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
2017-04-03 11:49:18 +00:00
end
2017-07-15 20:56:10 +00:00
--------------------------------------------------------------------------------
-- Text handling
2017-04-03 09:55:49 +00:00
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
2017-04-03 14:48:57 +00:00
local function add_indent_to_string ( str , indent )
2017-07-22 12:44:24 +00:00
-- Indents `str` by `indent`.
2017-07-14 14:51:19 +00:00
assert ( type ( str ) == ' string ' )
2017-06-05 21:24:24 +00:00
assert ( type ( indent ) == ' string ' )
2017-06-24 18:06:36 +00:00
return indent .. str : gsub ( ' \n ' , ' \n ' .. indent )
end
2017-06-24 18:37:43 +00:00
local function wrap_text ( text , max_width )
2017-07-22 12:44:24 +00:00
-- Creates a wrapped version of given `text`, with no lines longer than
-- `max_width`. Splits only at whitespace.
-- TODO: Fix this.
2017-06-24 18:37:43 +00:00
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
2017-04-03 09:55:49 +00:00
--------------------------------------------------------------------------------
2017-06-05 21:24:24 +00:00
return function ( value , depth , l , format_value )
assert ( type ( value ) == ' function ' )
assert ( type ( depth ) == ' number ' and type ( l ) == ' table ' and type ( format_value ) == ' function ' )
2017-04-03 09:55:49 +00:00
local info = get_function_info ( value )
2017-06-11 11:53:06 +00:00
local function_params , function_body = nil , ' ... '
2017-07-15 21:29:51 +00:00
if not info.docs and info.defined_how ~= ' C ' then
2017-07-15 20:56:10 +00:00
info = get_function_body_info ( info )
2017-07-15 20:11:16 +00:00
2017-07-15 21:29:51 +00:00
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
2017-07-15 20:56:10 +00:00
if info.defined_how == ' string ' then function_body = info.body end
end
2017-07-15 19:58:27 +00:00
end
2017-04-03 09:55:49 +00:00
2017-06-05 21:24:24 +00:00
if info.builtin and l.options . short_builtins then
2017-07-15 18:51:05 +00:00
assert ( info.name )
2017-06-11 11:53:06 +00:00
return l ( info.name ) ;
2017-04-03 11:56:06 +00:00
end
2017-04-03 09:55:49 +00:00
-- Include function modifier, and alignment info.
l [ # l + 1 ] = info.builtin and ' builtin ' or ' '
2017-04-14 12:01:01 +00:00
l [ # l + 1 ] = { ' align ' , ' func_mod ' , # l [ # l ] }
2017-04-03 09:55:49 +00:00
-- Build rest of function signature
2017-06-11 11:53:06 +00:00
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
2017-04-14 12:01:01 +00:00
l [ # l + 1 ] = { ' align ' , ' func_def ' , width_of_strings_in_l ( l , top_before ) }
2017-04-03 09:55:49 +00:00
-- Cleanup and finish
2017-06-24 16:53:59 +00:00
if depth ~= 0 then
2017-06-11 11:53:06 +00:00
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 '
2017-04-03 11:49:18 +00:00
end
2017-04-03 09:55:49 +00:00
2017-04-03 11:49:18 +00:00
-- More info! --
2017-04-03 09:55:49 +00:00
2017-06-05 21:24:24 +00:00
local indent = ' \n ' .. l.options . indent
2017-04-03 14:48:57 +00:00
2017-04-03 11:49:18 +00:00
-- Name
if info.name then
2017-04-03 14:48:57 +00:00
l [ # l + 1 ] = indent
2017-04-03 11:49:18 +00:00
l [ # l + 1 ] = ' -- '
l [ # l + 1 ] = info.name
end
2017-04-03 09:55:49 +00:00
2017-07-15 19:58:27 +00:00
-- Doc
2017-07-15 20:56:10 +00:00
if info.docs then
2017-06-24 18:06:36 +00:00
l [ # l + 1 ] = ' \n '
2017-06-24 18:37:43 +00:00
local indent = l.options . indent .. ' -- '
2017-07-15 20:56:10 +00:00
local docs = not info.builtin and info.docs or wrap_text ( info.docs , 80 - # indent )
2017-06-24 18:37:43 +00:00
l [ # l + 1 ] = add_indent_to_string ( docs , indent )
2017-04-03 11:49:18 +00:00
end
2017-04-03 09:55:49 +00:00
2017-04-03 11:49:18 +00:00
-- source
2017-07-15 20:56:10 +00:00
if info.docs or info.name then -- Do nothing
2017-07-14 14:51:19 +00:00
elseif info.defined_how == ' string ' then
l [ # l + 1 ] = indent
l [ # l + 1 ] = ' -- Loaded from string '
elseif not info.builtin then
2017-04-03 14:48:57 +00:00
l [ # l + 1 ] = indent
2017-07-14 14:51:19 +00:00
l [ # l + 1 ] = ( ' -- Source file: \' %s \' ' ) : format ( info.short_src )
2017-04-03 11:49:18 +00:00
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 )
2017-04-03 09:55:49 +00:00
end
2017-04-03 11:49:18 +00:00
end
2017-04-03 09:55:49 +00:00
2017-04-03 11:49:18 +00:00
-- upvalues
2017-07-15 20:56:10 +00:00
if info.nups > 0 and ( not info.builtin and not info.docs ) then
2017-04-03 14:48:57 +00:00
l [ # l + 1 ] = indent
2017-07-14 14:51:19 +00:00
l [ # l + 1 ] = ' -- Up values: '
2017-06-05 21:24:24 +00:00
format_value ( info.ups , depth + 1 , l )
2017-04-03 11:49:18 +00:00
end
2017-07-15 19:58:27 +00:00
-- 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 ] = ' \n end '
2017-04-03 09:55:49 +00:00
end