diff --git a/function.lua b/function.lua index ee0960a..060f606 100644 --- a/function.lua +++ b/function.lua @@ -123,8 +123,8 @@ local function get_function_paramlist_and_body (str, start_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 function_params, function_body = str:sub(start_line_index, end_line_index):match('function%s*[a-zA-Z0-9_.]*%s*(%([a-zA-Z0-9_,. \t]*%))[ \t]*(.-)[ \t]*end') - --print(function_params, function_body) + local function_params, function_body = str:sub(start_line_index, end_line_index):match('.*function%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 @@ -160,11 +160,13 @@ local function add_indent_to_string (str, indent) assert(type(indent) == 'string') local l = {} - for line in string.gmatch(str, '\n', true) do + for line in str:gmatch('[^\n]+', true) do l[#l+1] = indent l[#l+1] = line + l[#l+1] = '\n' end - return table.concat(l, '\n') + if l[#l] == '\n' then l[#l] = nil end + return table.concat(l, '') end -------------------------------------------------------------------------------- @@ -204,7 +206,7 @@ return function (value, depth, l, format_value) local info = get_function_info(value) - if l.options.include_closure and not info.builtin then + if l.options._include_closure and not info.builtin then return format_function_with_closure(value, depth, l, format_value) end @@ -237,7 +239,7 @@ return function (value, depth, l, format_value) l[#l+1] = { 'align', 'func_def', width_of_strings_in_l(l, top_before) } -- Cleanup and finish - if not l.options.more_function_info or depth ~= 0 then + 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 } @@ -288,16 +290,22 @@ return function (value, depth, l, format_value) -- NOTE: This is for testing/debugging/experimentation purposes, and is -- not designed to be pretty. + -- Native l[#l+1] = indent - l[#l+1] = '--[[ Function Body:\n\t' + 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] = '--[[\n\tNative repr:' - l[#l+1] = tostring(value) - 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] = '--]]' diff --git a/pretty.lua b/pretty.lua index a45ebf2..4498a53 100644 --- a/pretty.lua +++ b/pretty.lua @@ -463,19 +463,19 @@ setmetatable(StringBuilder, { -------------------------------------------------------------------------------- -local DEBUG_OPTIONS = { _all_function_info = true, _table_addr_comment = true } +local DEBUG_OPTION_USED = { } local KNOWN_OPTIONS = { - _table_addr_comment = { type = 'boolean', default = false }, - _all_function_info = { type = 'boolean', default = false }, + _table_addr_comment = { type = 'boolean', default = false, debug = 'debug' }, + _all_function_info = { type = 'boolean', default = false, debug = 'debug' }, + _include_closure = { type = 'boolean', default = false, debug = 'experimental' }, + cut_strings = { type = 'boolean', default = false }, - include_closure = { type = 'boolean', default = false }, indent = { type = 'string', default = ' ' }, max_depth = { type = 'number', default = math.huge }, - embed_loaded_funcs = { type = 'boolean', default = false }, - more_function_info = { type = 'boolean', default = false }, + embed_loaded_funcs = { type = 'boolean', default = false }, -- TODO: Outphase this, in favor of automatically embedding "small enough" functions. recursion = { type = 'string', default = 'ignore', accepted = {['ignore'] = true, ['marked'] = true} }, - short_builtins = { type = 'boolean', default = false }, + short_builtins = { type = 'boolean', default = false }, -- TODO: Outphase this. Rather automatically use the short versions in places where it would be strange to find the function, like keys, etc. } local function ensure_that_all_options_are_known (options) @@ -488,6 +488,9 @@ local function ensure_that_all_options_are_known (options) error(('[pretty]: Bad value given to option %s: %s (%s). Expected value of type %s'):format(option_name, option_value, type(option_value), KNOWN_OPTIONS[option_name].type), 2) elseif KNOWN_OPTIONS[option_name].accepted and not KNOWN_OPTIONS[option_name].accepted[option_value] then error(('[pretty]: Bad value given to option %s: %s (%s). Expected one of: %s'):format(option_name, option_value, type(option_value), table.concat(KNOWN_OPTIONS[option_name].accepted, ', ')), 2) + elseif KNOWN_OPTIONS[option_name].debug and not DEBUG_OPTION_USED[option_name] then + DEBUG_OPTION_USED[option_name] = true + print(('[pretty]: Using %s option "%s".\n Please note that this option may change at any time. It is not stable,\n not tested, and may indeed break or be removed without warning.'):format(KNOWN_OPTIONS[option_name].debug, option_name)) end end -- Assign default values diff --git a/test/test_function.lua b/test/test_function.lua index 0419fc5..eab743f 100644 --- a/test/test_function.lua +++ b/test/test_function.lua @@ -10,217 +10,153 @@ local HAS_ADV_GETLOCAL = not not debug.getinfo(1, 'u').nparams -- Lua 5.1 compa if not loadstring then loadstring = load end -- Lua 5.3 compat local function format_test (t) - if t.longterm then return end if t.adv_getlocal and not HAS_ADV_GETLOCAL then return end + if t.single then + return SUITE:addTest(t.name or t.expect, function () + local format = format({t.input}, t.options):gsub('^{%s+(.-)%s+}$', function(a) return '{ '..a..' }' end) + assert_equal('{ '..t.expect..' }', format) + end, { line = debug.getinfo(2).currentline }) + end SUITE:addTest(t.name or t.expect, function () - local input_value = t.input - local input_options = t.options - local expected_result = t.expect - local actual_result = format(input_value, input_options) - if not t.approx or type(actual_result) ~= 'string' then - assert_equal(expected_result, actual_result) - else - if not actual_result:match(expected_result) then - error(ASSERT_ERROR_APPROX:format(expected_result, actual_result)) - end - end + assert_equal(t.expect, format(t.input, t.options)) end, { line = debug.getinfo(2).currentline }) end -local function curline (delta) - return debug.getinfo(2).currentline + (delta or 0) -end +local function curline (delta) return debug.getinfo(2).currentline + (delta or 0) end + +local TEST_UPVALUE = 42 -------------------------------------------------------------------------------- --- Function printing +-- Basic inline functions format_test { + name = 'Basic function formatting', adv_getlocal = true, + single = true, input = function () end, expect = 'function () ... end', } format_test { + name = 'Function with a single argument', adv_getlocal = true, + single = true, input = function (a) end, expect = 'function (a) ... end', } format_test { + name = 'Function with multiple arguments', adv_getlocal = true, + single = true, input = function (a, b) end, expect = 'function (a, b) ... end', } format_test { + name = 'Function with vararg', adv_getlocal = true, + single = true, input = function (...) end, expect = 'function (...) ... end', } format_test { + name = 'Function with vararg and multiple arguments', adv_getlocal = true, + single = true, input = function (a, b, ...) end, expect = 'function (a, b, ...) ... end', } -do - local SOME_RANDOM_UPVALUE = false - - format_test { - adv_getlocal = true, - input = function () l = SOME_RANDOM_UPVALUE end, - expect = 'function () ... end', - } - - format_test { - adv_getlocal = true, - input = function () SOME_RANDOM_UPVALUE = true end, - expect = 'function () ... end', - } - - format_test { - -- More function info is ignored if not at depth 0. - adv_getlocal = true, - input = { a = function () SOME_RANDOM_UPVALUE = true end }, - options = { more_function_info = true }, - expect = '{ a = function () ... end }', - } - - - local func_line = curline(2) -- Must be exactly 2 lines above function - format_test { - input = function () l = SOME_RANDOM_UPVALUE end, - adv_getlocal = true, - options = { more_function_info = true }, - expect = 'function ()\n -- source_file: \'./test/test_function.lua\' [Line: '..func_line..']\n -- up_values: { SOME_RANDOM_UPVALUE = false }\n\n ...\nend' - } - - local func_line = curline(2) -- Must be exactly 2 lines above function - format_test { - input = function () SOME_RANDOM_UPVALUE = true end, - adv_getlocal = true, - options = { more_function_info = true }, - expect = 'function ()\n -- source_file: \'./test/test_function.lua\' [Line: '..func_line..']\n -- up_values: { SOME_RANDOM_UPVALUE = false }\n\n ...\nend' - } -end - -do - local func_line = curline(2) -- Must be exactly 2 lines above function - format_test { - input = function () end, - adv_getlocal = true, - options = { more_function_info = true }, - expect = 'function ()\n -- source_file: \'./test/test_function.lua\' [Line: '..func_line..']\n\n ...\nend' - } -end - -do - local index = 0 - - format_test { - input = function () index = index + 1; return index end, - adv_getlocal = true, - expect = 'function () ... end' - } - - local func_line = curline(2) -- Must be exactly 2 lines above function - format_test { - input = function () index = index + 1; return index end, - adv_getlocal = true, - options = { more_function_info = true }, - expect = 'function ()\n -- source_file: \'./test/test_function.lua\' [Line: '..func_line..']\n -- up_values: { index = 0 }\n\n ...\nend' - } -end - format_test { + name = 'Closures don\'t affect functions printed in single mode 1', adv_getlocal = true, - options = { embed_loaded_funcs = true }, - input = loadstring('return function () end')(), - expect = 'function () end', + single = true, + input = function () l = TEST_UPVALUE end, + expect = 'function () ... end', } format_test { + name = 'Closures don\'t affect functions printed in single mode 2', adv_getlocal = true, - options = { embed_loaded_funcs = true }, - input = loadstring('return function () return function () end end')(), - expect = 'function () return function () end end', + single = true, + input = function () TEST_UPVALUE = true end, + expect = 'function () ... end', } +-------------------------------------------------------------------------------- +-- Basic elaborate functions + +local func_line = curline(3) format_test { + name = 'Singleline elaborate function', + input = function () end, + expect = 'function ()\n -- source_file: \'./test/test_function.lua\' [Line: '..func_line..']\n\n ...\nend', +} + +local func_line = curline(3) +format_test { + name = 'Multiline elaborate function', + input = function () + end, + expect = 'function ()\n -- source_file: \'./test/test_function.lua\' [Lines: '..func_line..' - '..(func_line+1)..']\n\n ...\nend', +} + +local func_line = curline(3) -- Must be exactly 3 lines above function +format_test { + name = 'Elaborate function with upvalue included 1', + input = function () l = TEST_UPVALUE end, adv_getlocal = true, - options = { embed_loaded_funcs = true }, - input = loadstring('return function () return function () end\nend')()(), - expect = 'function () end', + expect = 'function ()\n -- source_file: \'./test/test_function.lua\' [Line: '..func_line..']\n -- up_values: { TEST_UPVALUE = 42 }\n\n ...\nend' } +local func_line = curline(3) -- Must be exactly 3 lines above function format_test { - -- NOTE: This is HARD to fix. It's thus longerterm + name = 'Elaborate function with upvalue included 2', + input = function () TEST_UPVALUE = true end, adv_getlocal = true, - options = { embed_loaded_funcs = true }, - input = loadstring('return function () return function () end end')()(), - expect = 'function () end', + expect = 'function ()\n -- source_file: \'./test/test_function.lua\' [Line: '..func_line..']\n -- up_values: { TEST_UPVALUE = 42 }\n\n ...\nend' } +local func_line = curline(3) format_test { - -- More function info allows one to even get the function whole, if it was defined in a string. - input = loadstring('return function (a, b) return a + b end')(), - options = { embed_loaded_funcs = true }, - expect = 'function (a, b) return a + b end', + name = 'Elaborate function with documentation', + input = function () + -- Hello World + if true then return false end + return true + end, + expect = 'function ()\n -- source_file: \'./test/test_function.lua\' [Lines: '..func_line..' - '..(func_line+1)..']\n -- Hello World\n\n ...\nend', } -format_test { - -- More function info allows one to even get the function whole, if it was defined in a string. - input = loadstring('return function (a, b)\n return a + b\nend')(), - options = { embed_loaded_funcs = true }, - expect = 'function (a, b)\n return a + b\nend', -} - -do - local func_line = curline(2) -- Must be exactly 2 lines above function - format_test { - input = function () - -- NOTE: This function must cover 3 lines of code! - end, - adv_getlocal = true, - options = { more_function_info = true }, - expect = 'function ()\n -- source_file: \'./test/test_function.lua\' [Lines: '..func_line..' - '..(func_line+2)..']\n\n ...\nend', - } - - local func_line = curline(2) -- Must be exactly 2 lines above function - format_test { - input = function () --[[ NOTE: This function must cover a single line of code! ]] end, - adv_getlocal = true, - options = { more_function_info = true }, - expect = 'function ()\n -- source_file: \'./test/test_function.lua\' [Line: '..func_line..']\n\n ...\nend', - } -end +-------------------------------------------------------------------------------- +-- Builtins format_test { + single = true, input = math.abs, expect = 'builtin function (x) ... end', } format_test { input = math.abs, - options = { more_function_info = true }, expect = 'builtin function (x)\n -- math.abs\n -- Returns the absolute value of x.\n\n ...\nend', } format_test { + single = true, input = math.random, expect = 'builtin function ([m [, n]) ... end', } format_test { input = math.random, - options = { more_function_info = true }, expect = 'builtin function ([m [, n])\n -- math.random\n -- When called without arguments, returns a uniform pseudo-random real number in the range [0,1). When called with an integer number m, math.random returns a uniform pseudo-random integer in the range [1, m]. When called with two integer numbers m and n, math.random returns a uniform pseudo-random integer in the range [m, n].\n\n ...\nend', } format_test { input = string.byte, - options = { more_function_info = true }, expect = 'builtin function (s [, i [, j]])\n -- string.byte\n -- 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 -- Note that numerical codes are not necessarily portable across platforms.\n\n ...\nend', } @@ -228,126 +164,69 @@ format_test { -- in a standard enviroment, return that name, instead of other complex info. format_test { + -- NOTE: These tests may be counter to intention, soon. input = math.random, options = { short_builtins = true }, expect = 'math.random', } format_test { + -- NOTE: These tests may be counter to intention, soon. input = { math.cos, math.sin, math.abs }, options = { short_builtins = true }, expect = '{ math.cos, math.sin, math.abs }', } +format_test { + name = 'Replace function with short version, if key', + input = { [math.random] = 365 }, + expect = '{ [math.random] = 365 }', +} + + -------------------------------------------------------------------------------- --- 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 +-- Embedding functions loaded with loadstring 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)()', + single = true, + name = 'It\'s possible to get loadstring functions whole', + input = loadstring('return function (a, b) return a + b end')(), + options = { embed_loaded_funcs = true }, + expect = 'function (a, b) return a + b 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)()', + single = true, + name = 'Whitespace is automatically stripped from loadstring functions', + input = loadstring('return function (a, b)\n return a + b\nend')(), + options = { embed_loaded_funcs = true }, + expect = 'function (a, b) return a + b 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 - +format_test { + single = true, + adv_getlocal = true, + options = { embed_loaded_funcs = true }, + input = loadstring('return function () return function () end\nend')()(), + expect = 'function () end', +} format_test { - name = 'Closures do not affect builtins', - input = math.abs, - options = { more_function_info = true, include_closure = true }, - expect = 'builtin function (x)\n -- math.abs\n -- Returns the absolute value of x.\n\n ...\nend', + name = 'When finding the correct function becomes too hard, just ignore it 1', + single = true, + adv_getlocal = true, + options = { embed_loaded_funcs = true }, + input = loadstring('return function () return function () end end')(), + expect = 'function () ... end', +} + +format_test { + name = 'When finding the correct function becomes too hard, just ignore it 2', + single = true, + adv_getlocal = true, + options = { embed_loaded_funcs = true }, + input = loadstring('return function () return function () end end')()(), + expect = 'function () ... end', } -------------------------------------------------------------------------------- @@ -400,13 +279,124 @@ format_test { expect = '{\n a = function (ψ) ... end\n b = function (a) ... end\n}', } +-------------------------------------------------------------------------------- +-- 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 + +-- NOTE: The following tests are all EXPERIMENTAL! All of the tests use features +-- which may change at any time. + +format_test { + name = 'Closures do not affect non-upvalue function', + adv_getlocal = true, + options = { _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', +} + +format_test { + name = 'Closures do not affect builtins', + input = math.abs, + options = { _include_closure = true }, + expect = 'builtin function (x)\n -- math.abs\n -- Returns the absolute value of x.\n\n ...\nend', +} + +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 = { _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 = { _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 = { _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 = { _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 = { _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 = { _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 -------------------------------------------------------------------------------- --[[ format_test { adv_getlocal = true, - options = { more_function_info = true, _all_function_info = true, max_depth = 2 }, + options = { _all_function_info = true, max_depth = 2 }, input = function() end, expect = '', }