dbg/dbg.lua

317 lines
7.5 KiB
Lua

local debug = ...
function dbg.locals(level)
level = (level or 1) + 1
local idx = {}
do
local i = 1
while true do
local name = debug.getlocal(level, i)
if name == nil then break end
idx[name] = i
i = i + 1
end
end
return setmetatable({}, {
__index = function(_, name)
local _, value = debug.getlocal(level, assert(idx[name], "no variable with given name"))
return value
end,
__newindex = function(_, name, value)
debug.setlocal(level, assert(idx[name], "no variable with given name"), value)
end,
__call = function()
local i = 0
local function iterate()
i = i + 1
-- Making this a tail call requires passing `level - 1`
local name, value = debug.getlocal(level, i)
return name, value
end
return iterate
end
})
end
function dbg.upvals(func)
if type(func) ~= "function" then
func = debug.getinfo((func or 1) + 1, "f").func
end
local idx = {}
do
local i = 1
while true do
local name = debug.getupvalue(func, i)
if name == nil then break end
idx[name] = i
i = i + 1
end
end
return setmetatable({}, {
__index = function(_, name)
local _, value = debug.getupvalue(func, assert(idx[name], "no upval with given name"))
return value
end,
__newindex = function(_, name, value)
debug.setupvalue(func, assert(idx[name], "no upval with given name"), value)
end,
__call = function()
local i = 0
local function iterate()
i = i + 1
return debug.getupvalue(func, i)
end
return iterate
end
})
end
function dbg.vars(level)
level = (level or 1) + 1
local func = debug.getinfo(level, "f").func
local idx, is_local = {}, {}
-- Upvals
do
local i = 1
while true do
local name = debug.getupvalue(func, i)
if name == nil then break end
idx[name] = i
i = i + 1
end
end
-- Locals
do
local i = 1
while true do
local name = debug.getlocal(level, i)
if name == nil then break end
idx[name] = i
is_local[name] = true -- might shadow upval
i = i + 1
end
end
return setmetatable({}, {
__index = function(_, name)
local var_idx = assert(idx[name], "no variable with given name")
local _, value
if is_local[name] then
_, value = debug.getlocal(level, var_idx)
else
_, value = debug.getupvalue(func, var_idx)
end
return value
end,
__newindex = function(_, name, value)
local var_idx = assert(idx[name], "no variable with given name")
if is_local[name] then
debug.setlocal(level, var_idx, value)
else
debug.setupvalue(func, var_idx, value)
end
end,
__call = function()
local i, upvals = 1, true
local function iterate()
local name, value
if upvals then
repeat -- search for not-shadowed upvals
name, value = debug.getupvalue(func, i)
i = i + 1
until not is_local[name]
if name == nil then
i, upvals = 1, false
end
end
if not upvals then
name, value = debug.getlocal(level, i)
i = i + 1
end
return name, value
end
return iterate
end
})
end
-- Roughly the same format as used by debug.traceback, but paths are shortened
local function fmt_callinfo(level)
local info = debug.getinfo(level, "Snlf")
if not info then
return
end
local is_path = info.source:match"^@"
local short_src = is_path and dbg.shorten_path(info.short_src) or info.short_src
local where
if (info.namewhat or "") ~= "" then
where = "in function " .. info.name
elseif info.what == "Lua" then
where = ("in function defined at line %d"):format(info.linedefined)
elseif info.what == "main" then
where = "in main chunk"
else
where = "?"
end
return short_src .. ":" .. (info.currentline > 0 and ("%d:"):format(info.currentline) or "") .. " " .. where, info.func
end
local max_top_levels, max_bottom_levels = 5, 5
function dbg._traceback(level, until_func --[[and including]])
level = (level or 1) + 1
local res = {"stack traceback:"}
local function concat() return table.concat(res, "\n\t") end
-- Write top levels
for top_level = 1, max_top_levels do
local str, func = fmt_callinfo(level + top_level)
if not str then return concat() end
table.insert(res, str)
if func == until_func then return concat() end
end
local last_written_top_level = level + max_top_levels
-- Determine stack depth
level = last_written_top_level
repeat
level = level + 1
until not debug.getinfo(level, "")
-- Write bottom levels
local first_bottom_level = level - max_bottom_levels
if last_written_top_level + 1 > first_bottom_level then
first_bottom_level = last_written_top_level + 1
else
table.insert(res, "...")
end
for bottom_level = first_bottom_level, level - 1 do
local str, func = fmt_callinfo(bottom_level)
table.insert(res, str)
if func == until_func then return concat() end
end
return concat()
end
-- Hide until_func parameter
function dbg.traceback(level)
return dbg._traceback(level)
end
function dbg.stackinfo(level)
local res = {}
while true do
local info = debug.getinfo(level)
if not info then return res end
table.insert(res, info)
end
end
--! Only available on Lua 5.2 / LuaJIT; use the `arg` local on Lua 5.1 instead
function dbg.getvararg(level)
level = (level or 1) + 1
local function _getvararg(i)
local name, value = debug.getlocal(level, i)
if not name then return end
return value, _getvararg(i - 1)
end
return _getvararg(-1)
end
-- Test dbg.getvararg to set it to `nil` if it isn't supported
(function(...) -- luacheck: ignore
local args = {dbg.getvararg()}
if #args == 3 then
for i = 1, 3 do
if args[i] ~= i then
dbg.getvararg = nil
break
end
end
else
dbg.getvararg = nil
end
end)(1, 2, 3)
local function nils(n)
if n == 1 then return nil end
return nil, nils(n - 1)
end
local function getargs(func)
-- This function must be explicitly handled
-- as otherwise the first call to it might trigger a false-positive in the hook
if func == nils then return {"n"} end
local what = debug.getinfo(func, "S").what
if what == "C" then return {"?"} end
if what == "main" then return {"..."} end
assert(what == "Lua")
local hook, mask, count = debug.gethook()
local args = {}
debug.sethook(function()
local called_func = debug.getinfo(2, "f")
if called_func.func ~= func then return end
local i = 1
while true do
local name = debug.getlocal(2, i)
if name == nil then break end
if not name:match"^%(" then table.insert(args, name) end
i = i + 1
end
error(args)
end, "c")
local status, _args = pcall(func)
assert(not status and args == _args)
-- Check for vararg by supplying one extraneous param
debug.sethook(function()
local called_func = debug.getinfo(2, "f")
if called_func.func ~= func then return end
if debug.getlocal(2, -1) then -- vararg
table.insert(args, "...")
end
error(args)
end, "c")
status, _args = pcall(func, nils(#args + 1))
assert(not status and args == _args)
debug.sethook(hook, mask, count) -- restore previous hook
return args
end
local function shallowequals(t1, t2)
for k, v in pairs(t1) do
if t2[k] ~= v then return false end
end
return true
end
-- Tests to check for PUC Lua 5.1 unreliability
-- Ignore "unused argument" warnings
-- luacheck: push ignore 212
local function test_getargs()
for func, expected_arg_list in pairs{
[function(x, y, z)end] = {"x", "y", "z"}, -- unused arguments
[function(...)end] = {"..."}, -- unused vararg
[function(x, y, z, ...)end] = {"x", "y", "z", "..."}, -- both
[nils] = {"n"}
} do
if not shallowequals(getargs(func), expected_arg_list) then
return false
end
end
return true
end
-- luacheck: pop
dbg.getargs = getargs
dbg.getargs_reliable = test_getargs()