diff --git a/function.lua b/function.lua index 46e6677..9ca29fd 100644 --- a/function.lua +++ b/function.lua @@ -63,11 +63,20 @@ local function get_line_index (str, line_nr) end local function get_full_function_str (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`. local start_line_index = get_line_index(str, start_line) local end_line_index = get_line_index(str, end_line + 1) - local matched_function = str:sub(start_line_index, end_line_index):match('function.+end') - return matched_function + local function_body = str:sub(start_line_index, end_line_index):match('function%s*[a-zA-Z0-9_.]*%s*(.+)end') + return 'function '..function_body..'end' +end + +local function get_function_str_from_file (filename, start_line, end_line) + 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) @@ -81,9 +90,40 @@ end -------------------------------------------------------------------------------- +local function format_function_with_closure (value, options, depth, l, format_value) + local info = get_function_info(value) + + --assert(info.nups > 0) + 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, options, 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, options, depth, l, format_value) local info = get_function_info(value) + if options.include_closure then + return format_function_with_closure(value, options, depth, l, format_value) + end + if info.defined_how == 'string' then -- Function was defined as a string. l[#l+1] = get_full_function_str(info.source, info.linedefined, info.lastlinedefined) @@ -105,12 +145,54 @@ return function (value, options, depth, l, format_value) -- Cleanup and finish if not options.more_function_info or depth ~= 0 then l[#l+1] = ' ... end' - elseif options._all_function_info then + return; + end + + -- More info! -- + + -- Name + if info.name then + l[#l+1] = '\n' + l[#l+1] = options.indent + l[#l+1] = '-- ' + l[#l+1] = info.name + end + + -- Doc + if info.doc then + for doc_line in info.doc:gmatch('[^\n]+') do + l[#l+1] = '\n' + l[#l+1] = options.indent + l[#l+1] = '-- ' + + l[#l+1] = doc_line + end + end + + -- source + if not info.builtin then + l[#l+1] = '\n' + l[#l+1] = options.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 then + l[#l+1] = '\n' + l[#l+1] = options.indent + l[#l+1] = '-- up_values: ' + format_value(info.ups, options, depth + 1, l) + end + + if options._all_function_info then -- NOTE: This is for testing/debugging/experimentation purposes. - local file = io.open(info.short_src, 'r') - local function_str = get_full_function_str(file:read('*all'), info.linedefined, info.lastlinedefined) - file:close() + local function_str = get_function_str_from_file(info.short_src, info.linedefined, info.lastlinedefined) l[#l+1] = '\n\t--[[ Function Body\n\t' l[#l+1] = function_str @@ -121,57 +203,9 @@ return function (value, options, depth, l, format_value) l[#l+1] = '\n\t' format_value(info, options, depth + 1, l) l[#l+1] = '--]]' - else - -- More info! -- - - -- Name - if info.name then - l[#l+1] = '\n' - l[#l+1] = options.indent - l[#l+1] = '-- ' - l[#l+1] = info.name - end - - -- Doc - if info.doc then - for doc_line in info.doc:gmatch('[^\n]+') do - l[#l+1] = '\n' - l[#l+1] = options.indent - l[#l+1] = '-- ' - - l[#l+1] = doc_line - end - end - - -- source - if not info.builtin then - l[#l+1] = '\n' - l[#l+1] = options.indent - l[#l+1] = '-- source_file: \'' - l[#l+1] = info.short_src - l[#l+1] = '\' [Line' - if info.linedefined == info.lastlinedefined then - l[#l+1] = ': ' - l[#l+1] = tostring(info.linedefined) - else - l[#l+1] = 's: ' - l[#l+1] = tostring(info.linedefined) - l[#l+1] = ' - ' - l[#l+1] = tostring(info.lastlinedefined) - end - l[#l+1] = ']' - end - - -- upvalues - if info.nups > 0 and not info.builtin then - l[#l+1] = '\n' - l[#l+1] = options.indent - l[#l+1] = '-- up_values: ' - format_value(info.ups, options, depth + 1, l) - end - - l[#l+1] = '\n\n' - l[#l+1] = options.indent - l[#l+1] = '...\nend' end + + l[#l+1] = '\n\n' + l[#l+1] = options.indent + l[#l+1] = '...\nend' end diff --git a/test/test_function.lua b/test/test_function.lua index 6a6575f..a8d8b0e 100644 --- a/test/test_function.lua +++ b/test/test_function.lua @@ -12,7 +12,7 @@ if not loadstring then loadstring = load end -- Lua 5.3 compa local function format_test (t) if t.longterm then return end if t.adv_getlocal and not HAS_ADV_GETLOCAL then return end - SUITE:addTest(t.expect, function () + SUITE:addTest(t.name or t.expect, function () local input_value = t.input local input_options = t.options local expected_result = t.expect @@ -220,6 +220,126 @@ format_test { expect = 'builtin function (s [, i [, j]])\n\t-- string.byte\n\t-- Returns the internal numerical codes of the characters s[i], s[i+1], ..., s[j]. The default value for i is 1; the default value for j is i.\n\t-- Note that numerical codes are not necessarily portable across platforms.\n\n\t...\nend', } +-- short_builtins option: If an builtin is expected to be available by some name +-- in a standard enviroment, return that name, instead of other complex info. + +format_test { + input = math.random, + options = { short_builtins = true }, + expect = 'math.random', +} + +format_test { + input = { math.cos, math.sin, math.abs }, + options = { short_builtins = true }, + expect = '{ math.cos, math.sin, math.abs }', +} + +-------------------------------------------------------------------------------- +-- Closure creation +-- include_closure option: If a function uses upvalues, return code that creates +-- a closure for that function, including the function code. + +-- NOTE: Without AST traversal, it's impossible to garentee that values refered +-- by multiple recursive closures are the same, because they can actually refer +-- to different variables with the same name, as the following example shows: +-- +-- local val_a = 1 +-- local func_a = function () return val_a end +-- local val_a = 2 +-- local func_c = function () return func_a() + val_a end + +do + format_test { + adv_getlocal = true, + options = { more_function_info = true, include_closure = true }, + input = function (a, b) return (a > b) and a or b end, + expect = 'function (a, b) return (a > b) and a or b end', + } +end + +do + local input_func = (function () + local i = 0 + return function (a) i = i + a; return i end + end)() + for i = 1, 10 do input_func(1) end + + format_test { + adv_getlocal = true, + options = { more_function_info = true, include_closure = true }, + input = input_func, + expect = '(function () local i = 10; return function (a) i = i + a; return i end end)()', + } +end + +do + local input_func = loadstring([[ + return function () + local i = 0 + return function (a) i = i + a; return i end + end]])()() + for i = 1, 10 do input_func(1) end + + format_test { + name = 'Can include variables in closure', + adv_getlocal = true, + options = { more_function_info = true, include_closure = true }, + input = input_func, + expect = '(function () local i = 10; return function (a) i = i + a; return i end end)()', + } +end + +format_test { + name = 'Can include functions in closure', + adv_getlocal = true, + options = { more_function_info = true, include_closure = true }, + input = (function () + local custom_max = function (a, b) return (a > b) and a or b end + return function (a, b) return custom_max(a, b) + 2 end + end)(), + expect = '(function () local custom_max = function (a, b) return (a > b) and a or b end; return function (a, b) return custom_max(a, b) + 2 end end)()', +} + +format_test { + name = 'Can include functions defined with `function () ...` style', + adv_getlocal = true, + options = { more_function_info = true, include_closure = true }, + input = (function () + local function custom_max (a, b) return (a > b) and a or b end + return function (a, b) return custom_max(a, b) + 2 end + end)(), + expect = '(function () local custom_max = function (a, b) return (a > b) and a or b end; return function (a, b) return custom_max(a, b) + 2 end end)()', +} + +do + local custom_max = function (a, b) return (a > b) and a or b end + format_test { + name = 'Can include functions from outside scope into closure', + adv_getlocal = true, + options = { more_function_info = true, include_closure = true }, + input = (function () + return function (a, b) return custom_max(a, b) + 2 end + end)(), + expect = '(function () local custom_max = function (a, b) return (a > b) and a or b end; return function (a, b) return custom_max(a, b) + 2 end end)()', + } +end + +do + local a_func = function (x) return x + 2 end + local b_func = function (x) return a_func(x) * a_func(x) end + local c_func = function (x) return b_func(a_func(x)) end + format_test { + name = 'Can support recursive closures', + adv_getlocal = true, + options = { more_function_info = true, include_closure = true }, + input = c_func, + expect = '(function () local b_func = (function () local a_func = function (x) return x + 2 end; return function (x) return a_func(x) * a_func(x) end end)(); local a_func = function (x) return x + 2 end; return function (x) return b_func(a_func(x)) end end)()', + } +end + + +-------------------------------------------------------------------------------- -- Indent functions nicely format_test { @@ -244,6 +364,16 @@ format_test { expect = '{\n\tabs = function (x) ... end,\n\tmax = function (a, b) ... end\n}', } + -------------------------------------------------------------------------------- +--[[ +format_test { + adv_getlocal = true, + options = { more_function_info = true, _all_function_info = true, max_depth = 2 }, + input = function() end, + expect = '', +} +--]] + return SUITE