2017-04-06 12:46:45 +00:00
--[=[ 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 .
--]=]
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.
local FUNCTION_DEFINITION_MATCH =
2017-07-15 18:59:55 +00:00
' .*%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
2017-07-15 18:43:25 +00:00
' end ' -- Look for the end keyword
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 '
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
info.doc = LIBRARY [ f ] . docs
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-14 14:51:19 +00:00
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.
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 ' )
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 )
2017-04-03 09:55:49 +00:00
2017-07-14 14:51:19 +00:00
-- Now find the function parameters and the function body.
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 )
-- 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
2017-06-24 16:53:59 +00:00
function_body = function_body : match ( ' ^%s*(.-)%s*$ ' )
2017-07-15 18:43:25 +00:00
assert ( type ( function_body ) == ' string ' )
2017-07-14 14:51:19 +00:00
-- And return them.
2017-06-11 11:53:06 +00:00
return function_params , function_body
end
2017-07-14 14:51:19 +00:00
local function get_function_string ( ... )
return string.format ( ' function %s %s end ' , get_function_paramlist_and_body ( ... ) )
2017-04-03 11:49:18 +00:00
end
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-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
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
2017-04-03 14:48:57 +00:00
end
2017-06-24 18:06:36 +00:00
return table.concat ( doc_lines , ' \n ' )
2017-04-03 14:48:57 +00:00
end
2017-06-24 18:37:43 +00:00
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
2017-04-03 09:55:49 +00:00
--------------------------------------------------------------------------------
2017-06-05 21:24:24 +00:00
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 ' )
2017-04-03 11:49:18 +00:00
local info = get_function_info ( value )
2017-07-14 14:51:19 +00:00
local function_str = get_function_string ( info )
2017-04-03 11:49:18 +00:00
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 ] = ' = '
2017-06-05 21:24:24 +00:00
format_value ( v , depth + 1 , l )
2017-04-03 11:49:18 +00:00
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
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-24 16:53:59 +00:00
if l.options . _include_closure and not info.builtin then
2017-06-05 21:24:24 +00:00
return format_function_with_closure ( value , depth , l , format_value )
2017-04-03 11:49:18 +00:00
end
2017-06-11 11:53:06 +00:00
local function_params , function_body = nil , ' ... '
2017-07-15 18:10:49 +00:00
if info.defined_how == ' string ' then
-- Function was defined as a string
2017-07-15 18:51:05 +00:00
local _ , body = get_function_paramlist_and_body ( info )
2017-07-15 18:10:49 +00:00
if # body <= NR_CHARS_IN_LONG_FUNCTION_BODY and not body : find ' \n ' then
2017-07-15 18:51:05 +00:00
function_body = body
2017-07-15 18:10:49 +00:00
end
2017-04-03 09:55:49 +00:00
end
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-04-03 11:49:18 +00:00
-- Doc
2017-06-24 18:06:36 +00:00
if not info.doc then
2017-07-14 14:51:19 +00:00
local function_body = select ( 2 , get_function_paramlist_and_body ( info ) )
2017-06-24 18:06:36 +00:00
if function_body then
local documentation = get_docs_from_function_body ( function_body )
info.doc = documentation ~= ' ' and documentation
end
end
2017-04-03 09:55:49 +00:00
2017-06-24 18:06:36 +00:00
if info.doc then
l [ # l + 1 ] = ' \n '
2017-06-24 18:37:43 +00:00
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 )
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-14 14:51:19 +00:00
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
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-06-24 18:06:36 +00:00
if info.nups > 0 and ( not info.builtin and not info.doc ) 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-06-05 21:24:24 +00:00
if l.options . _all_function_info then
2017-04-14 12:01:01 +00:00
-- NOTE: This is for testing/debugging/experimentation purposes, and is
-- not designed to be pretty.
2017-04-03 11:49:18 +00:00
2017-06-24 16:53:59 +00:00
-- Native
2017-04-03 14:48:57 +00:00
l [ # l + 1 ] = indent
2017-07-14 14:51:19 +00:00
l [ # l + 1 ] = ' -- Native Representation: '
2017-06-24 16:53:59 +00:00
l [ # l + 1 ] = tostring ( value )
-- Function body
2017-07-14 14:51:19 +00:00
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
2017-04-03 11:49:18 +00:00
2017-06-24 16:53:59 +00:00
-- Full info
2017-04-03 14:48:57 +00:00
l [ # l + 1 ] = indent
2017-06-24 16:53:59 +00:00
l [ # l + 1 ] = ' --[[ full_info: \n '
info.env = nil
2017-06-05 21:24:24 +00:00
format_value ( info , depth + 1 , l )
2017-04-03 14:48:57 +00:00
l [ # l + 1 ] = indent
2017-04-03 11:49:18 +00:00
l [ # l + 1 ] = ' --]] '
2017-04-03 09:55:49 +00:00
end
2017-04-03 11:49:18 +00:00
2017-04-03 14:48:57 +00:00
l [ # l + 1 ] = ' \n '
l [ # l + 1 ] = indent
2017-04-03 11:49:18 +00:00
l [ # l + 1 ] = ' ... \n end '
2017-04-03 09:55:49 +00:00
end