--[=[ 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 -------------------------------------------------------------------------------- -- 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 (str, start_line, end_line) -- Will attempt to find a string which refer to a function starting on -- line `start_line` and ending on line `end_line`. assert(type(str) == 'string') assert(type(start_line) == 'number') assert(type(end_line) == 'number') local start_line_index = get_line_index(str, start_line) local end_line_index = get_line_index(str, end_line + 1) local function_params, function_body = str:sub(start_line_index, end_line_index):match('.*%f[%a_]function%f[^%a_]%s*[a-zA-Z0-9_.]*%s*(%([a-zA-Z0-9_,. \t]*%))[ \t]*(.+)[ \t]*end') function_body = function_body:match('^%s*(.-)%s*$') assert(type(function_params) == 'string' and type(function_body) == 'string') return function_params, function_body end local function get_function_body_from_file (filename, start_line, end_line) assert(type(filename) == 'string') assert(type(start_line) == 'number') assert(type(end_line) == 'number') local file = io.open(filename, 'r') local str = file:read('*all') file:close() return select(2, get_function_paramlist_and_body(str, start_line, end_line)) end local function get_full_function_str (...) local function_params, function_body = get_function_paramlist_and_body(...) return 'function '..function_params..' '..function_body..' end' end local function get_function_str_from_file (filename, start_line, end_line) assert(type(filename) == 'string') assert(type(start_line) == 'number') assert(type(end_line) == 'number') local file = io.open(filename, 'r') local str = file:read('*all') file:close() return get_full_function_str(str, start_line, end_line) 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 = nil if (info.defined_how == 'string') then function_str = get_full_function_str(info.source, info.linedefined, info.lastlinedefined) else function_str = get_function_str_from_file(info.short_src, info.linedefined, info.lastlinedefined) end 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' and l.options.embed_loaded_funcs then -- Function was defined as a string. function_params, function_body = get_function_paramlist_and_body(info.source, info.linedefined, info.lastlinedefined) --elseif info.defined_how == 'file' then --function_body = get_function_body_from_file(info.short_src, info.linedefined, info.lastlinedefined) ----print(function_body) --function_body = function_body or '...' end if info.builtin and l.options.short_builtins then 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 = get_function_body_from_file(info.short_src, info.linedefined, info.lastlinedefined) 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 not info.builtin and not info.doc 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_repr: ' l[#l+1] = tostring(value) -- Function body l[#l+1] = indent l[#l+1] = '--[[ function_body:\n\t' l[#l+1] = add_indent_to_string(get_function_str_from_file(info.short_src, info.linedefined, info.lastlinedefined), l.options.indent) l[#l+1] = indent l[#l+1] = '--]]' -- 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