diff options
Diffstat (limited to '.config/awesome/quarrel/debugger.lua')
-rw-r--r-- | .config/awesome/quarrel/debugger.lua | 865 |
1 files changed, 865 insertions, 0 deletions
diff --git a/.config/awesome/quarrel/debugger.lua b/.config/awesome/quarrel/debugger.lua new file mode 100644 index 0000000..b1a272a --- /dev/null +++ b/.config/awesome/quarrel/debugger.lua @@ -0,0 +1,865 @@ +-- SPDX-License-Identifier: MIT +-- Copyright (c) 2024 Scott Lembcke and Howling Moon Software + +local dbg + +-- Use ANSI color codes in the prompt by default. +local COLOR_GRAY = "" +local COLOR_RED = "" +local COLOR_BLUE = "" +local COLOR_YELLOW = "" +local COLOR_RESET = "" +local GREEN_CARET = " => " + +local function pretty(obj, max_depth) + if max_depth == nil then + max_depth = dbg.pretty_depth + end + + -- Returns true if a table has a __tostring metamethod. + local function coerceable(tbl) + local meta = getmetatable(tbl) + return (meta and meta.__tostring) + end + + local function recurse(obj, depth) + if type(obj) == "string" then + -- Dump the string so that escape sequences are printed. + return string.format("%q", obj) + elseif type(obj) == "table" and depth < max_depth and not coerceable(obj) then + local str = "{" + + for k, v in pairs(obj) do + local pair = pretty(k, 0) .. " = " .. recurse(v, depth + 1) + str = str .. (str == "{" and pair or ", " .. pair) + end + + return str .. "}" + else + -- tostring() can fail if there is an error in a __tostring metamethod. + local success, value = pcall(function() + return tostring(obj) + end) + return (success and value or "<!!error in __tostring metamethod!!>") + end + end + + return recurse(obj, 0) +end + +-- The stack level that cmd_* functions use to access locals or info +-- The structure of the code very carefully ensures this. +local CMD_STACK_LEVEL = 6 + +-- Location of the top of the stack outside of the debugger. +-- Adjusted by some debugger entrypoints. +local stack_top = 0 + +-- The current stack frame index. +-- Changed using the up/down commands +local stack_inspect_offset = 0 + +-- LuaJIT has an off by one bug when setting local variables. +local LUA_JIT_SETLOCAL_WORKAROUND = 0 + +-- Default dbg.read function +local function dbg_read(prompt) + dbg.write(prompt) + io.flush() + return io.read() +end + +-- Default dbg.write function +local function dbg_write(str) + io.write(str) +end + +local function dbg_writeln(str, ...) + if select("#", ...) == 0 then + dbg.write((str or "<NULL>") .. "\n") + else + dbg.write(string.format(str .. "\n", ...)) + end +end + +local function format_loc(file, line) + return COLOR_BLUE .. file .. COLOR_RESET .. ":" .. COLOR_YELLOW .. line .. COLOR_RESET +end +local function format_stack_frame_info(info) + local filename = info.source:match "@(.*)" + local source = filename and dbg.shorten_path(filename) or info.short_src + local namewhat = (info.namewhat == "" and "chunk at" or info.namewhat) + local name = ( + info.name and "'" .. COLOR_BLUE .. info.name .. COLOR_RESET .. "'" or format_loc(source, info.linedefined) + ) + return format_loc(source, info.currentline) .. " in " .. namewhat .. " " .. name +end + +local repl + +-- Return false for stack frames without source, +-- which includes C frames, Lua bytecode, and `loadstring` functions +local function frame_has_line(info) + return info.currentline >= 0 +end + +local function hook_factory(repl_threshold) + return function(offset, reason) + return function(event, _) + -- Skip events that don't have line information. + if not frame_has_line(debug.getinfo(2)) then + return + end + + -- Tail calls are specifically ignored since they also will have tail returns to balance out. + if event == "call" then + offset = offset + 1 + elseif event == "return" and offset > repl_threshold then + offset = offset - 1 + elseif event == "line" and offset <= repl_threshold then + repl(reason) + end + end + end +end + +local hook_step = hook_factory(1) +local hook_next = hook_factory(0) +local hook_finish = hook_factory(-1) + +-- Create a table of all the locally accessible variables. +-- Globals are not included when running the locals command, but are when running the print command. +local function local_bindings(offset, include_globals) + local level = offset + stack_inspect_offset + CMD_STACK_LEVEL + local func = debug.getinfo(level).func + local bindings = {} + + -- Retrieve the upvalues + do + local i = 1 + while true do + local name, value = debug.getupvalue(func, i) + if not name then + break + end + bindings[name] = value + i = i + 1 + end + end + + -- Retrieve the locals (overwriting any upvalues) + do + local i = 1 + while true do + local name, value = debug.getlocal(level, i) + if not name then + break + end + bindings[name] = value + i = i + 1 + end + end + + -- Retrieve the varargs (works in Lua 5.2 and LuaJIT) + local varargs = {} + do + local i = 1 + while true do + local name, value = debug.getlocal(level, -i) + if not name then + break + end + varargs[i] = value + i = i + 1 + end + end + if #varargs > 0 then + bindings["..."] = varargs + end + + if include_globals then + -- In Lua 5.2, you have to get the environment table from the function's locals. + local env = (_VERSION <= "Lua 5.1" and getfenv(func) or bindings._ENV) + return setmetatable(bindings, { __index = env or _G }) + else + return bindings + end +end + +-- Used as a __newindex metamethod to modify variables in cmd_eval(). +local function mutate_bindings(_, name, value) + local FUNC_STACK_OFFSET = 3 -- Stack depth of this function. + local level = stack_inspect_offset + FUNC_STACK_OFFSET + CMD_STACK_LEVEL + + -- Set a local. + do + local i = 1 + repeat + local var = debug.getlocal(level, i) + if name == var then + dbg_writeln( + COLOR_YELLOW + .. "debugger.lua" + .. GREEN_CARET + .. "Set local variable " + .. COLOR_BLUE + .. name + .. COLOR_RESET + ) + return debug.setlocal(level + LUA_JIT_SETLOCAL_WORKAROUND, i, value) + end + i = i + 1 + until var == nil + end + + -- Set an upvalue. + local func = debug.getinfo(level).func + do + local i = 1 + repeat + local var = debug.getupvalue(func, i) + if name == var then + dbg_writeln( + COLOR_YELLOW .. "debugger.lua" .. GREEN_CARET .. "Set upvalue " .. COLOR_BLUE .. name .. COLOR_RESET + ) + return debug.setupvalue(func, i, value) + end + i = i + 1 + until var == nil + end + + -- Set a global. + dbg_writeln( + COLOR_YELLOW .. "debugger.lua" .. GREEN_CARET .. "Set global variable " .. COLOR_BLUE .. name .. COLOR_RESET + ) + _G[name] = value +end + +-- Compile an expression with the given variable bindings. +local function compile_chunk(block, env) + local source = "debugger.lua REPL" + local chunk = nil + + if _VERSION <= "Lua 5.1" then + chunk = loadstring(block, source) + if chunk then + setfenv(chunk, env) + end + else + -- The Lua 5.2 way is a bit cleaner + chunk = load(block, source, "t", env) + end + + if not chunk then + dbg_writeln(COLOR_RED .. "Error: Could not compile block:\n" .. COLOR_RESET .. block) + end + return chunk +end + +local SOURCE_CACHE = {} + +local function where(info, context_lines) + local source = SOURCE_CACHE[info.source] + if not source then + source = {} + local filename = info.source:match "@(.*)" + if filename then + pcall(function() + for line in io.lines(filename) do + table.insert(source, line) + end + end) + elseif info.source then + for line in info.source:gmatch "[^\n]+" do + table.insert(source, line) + end + end + SOURCE_CACHE[info.source] = source + end + + if source and source[info.currentline] then + for i = info.currentline - context_lines, info.currentline + context_lines do + local tab_or_caret = (i == info.currentline and GREEN_CARET or " ") + local line = source[i] + if line then + dbg_writeln(COLOR_GRAY .. "% 4d" .. tab_or_caret .. "%s", i, line) + end + end + else + dbg_writeln(COLOR_RED .. "Error: Source not available for " .. COLOR_BLUE .. info.short_src) + end + + return false +end + +-- Wee version differences +local unpack = unpack or table.unpack +local pack = function(...) + return { n = select("#", ...), ... } +end + +local function cmd_step() + stack_inspect_offset = stack_top + return true, hook_step +end + +local function cmd_next() + stack_inspect_offset = stack_top + return true, hook_next +end + +local function cmd_finish() + local offset = stack_top - stack_inspect_offset + stack_inspect_offset = stack_top + return true, offset < 0 and hook_factory(offset - 1) or hook_finish +end + +local function cmd_print(expr) + local env = local_bindings(1, true) + local chunk = compile_chunk("return " .. expr, env) + if chunk == nil then + return false + end + + -- Call the chunk and collect the results. + local results = pack(pcall(chunk, unpack(rawget(env, "...") or {}))) + + -- The first result is the pcall error. + if not results[1] then + dbg_writeln(COLOR_RED .. "Error:" .. COLOR_RESET .. " " .. results[2]) + else + local output = "" + for i = 2, results.n do + output = output .. (i ~= 2 and ", " or "") .. dbg.pretty(results[i]) + end + + if output == "" then + output = "<no result>" + end + dbg_writeln(COLOR_BLUE .. expr .. GREEN_CARET .. output) + end + + return false +end + +local function cmd_eval(code) + local env = local_bindings(1, true) + local mutable_env = setmetatable({}, { + __index = env, + __newindex = mutate_bindings, + }) + + local chunk = compile_chunk(code, mutable_env) + if chunk == nil then + return false + end + + -- Call the chunk and collect the results. + local success, err = pcall(chunk, unpack(rawget(env, "...") or {})) + if not success then + dbg_writeln(COLOR_RED .. "Error:" .. COLOR_RESET .. " " .. tostring(err)) + end + + return false +end + +local function cmd_down() + local offset = stack_inspect_offset + local info + + repeat -- Find the next frame with a file. + offset = offset + 1 + info = debug.getinfo(offset + CMD_STACK_LEVEL) + until not info or frame_has_line(info) + + if info then + stack_inspect_offset = offset + dbg_writeln("Inspecting frame: " .. format_stack_frame_info(info)) + if tonumber(dbg.auto_where) then + where(info, dbg.auto_where) + end + else + info = debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL) + dbg_writeln "Already at the bottom of the stack." + end + + return false +end + +local function cmd_up() + local offset = stack_inspect_offset + local info + + repeat -- Find the next frame with a file. + offset = offset - 1 + if offset < stack_top then + info = nil + break + end + info = debug.getinfo(offset + CMD_STACK_LEVEL) + until frame_has_line(info) + + if info then + stack_inspect_offset = offset + dbg_writeln("Inspecting frame: " .. format_stack_frame_info(info)) + if tonumber(dbg.auto_where) then + where(info, dbg.auto_where) + end + else + info = debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL) + dbg_writeln "Already at the top of the stack." + end + + return false +end + +local function cmd_inspect(offset) + offset = stack_top + tonumber(offset) + local info = debug.getinfo(offset + CMD_STACK_LEVEL) + if info then + stack_inspect_offset = offset + dbg.writeln("Inspecting frame: " .. format_stack_frame_info(info)) + else + dbg.writeln(COLOR_RED .. "ERROR: " .. COLOR_BLUE .. "Invalid stack frame index." .. COLOR_RESET) + end +end + +local function cmd_where(context_lines) + local info = debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL) + return (info and where(info, tonumber(context_lines) or 5)) +end + +local function cmd_trace() + dbg_writeln("Inspecting frame %d", stack_inspect_offset - stack_top) + local i = 0 + while true do + local info = debug.getinfo(stack_top + CMD_STACK_LEVEL + i) + if not info then + break + end + + local is_current_frame = (i + stack_top == stack_inspect_offset) + local tab_or_caret = (is_current_frame and GREEN_CARET or " ") + dbg_writeln(COLOR_GRAY .. "% 4d" .. COLOR_RESET .. tab_or_caret .. "%s", i, format_stack_frame_info(info)) + i = i + 1 + end + + return false +end + +local function cmd_locals() + local bindings = local_bindings(1, false) + + -- Get all the variable binding names and sort them + local keys = {} + for k, _ in pairs(bindings) do + table.insert(keys, k) + end + table.sort(keys) + + for _, k in ipairs(keys) do + local v = bindings[k] + + -- Skip the debugger object itself, "(*internal)" values, and Lua 5.2's _ENV object. + if not rawequal(v, dbg) and k ~= "_ENV" and not k:match "%(.*%)" then + dbg_writeln(" " .. COLOR_BLUE .. k .. GREEN_CARET .. dbg.pretty(v)) + end + end + + return false +end + +local function cmd_help() + dbg.write( + "" + .. COLOR_BLUE + .. " <return>" + .. GREEN_CARET + .. "re-run last command\n" + .. COLOR_BLUE + .. " c" + .. COLOR_YELLOW + .. "(ontinue)" + .. GREEN_CARET + .. "continue execution\n" + .. COLOR_BLUE + .. " s" + .. COLOR_YELLOW + .. "(tep)" + .. GREEN_CARET + .. "step forward by one line (into functions)\n" + .. COLOR_BLUE + .. " n" + .. COLOR_YELLOW + .. "(ext)" + .. GREEN_CARET + .. "step forward by one line (skipping over functions)\n" + .. COLOR_BLUE + .. " f" + .. COLOR_YELLOW + .. "(inish)" + .. GREEN_CARET + .. "step forward until exiting the current function\n" + .. COLOR_BLUE + .. " u" + .. COLOR_YELLOW + .. "(p)" + .. GREEN_CARET + .. "move up the stack by one frame\n" + .. COLOR_BLUE + .. " d" + .. COLOR_YELLOW + .. "(own)" + .. GREEN_CARET + .. "move down the stack by one frame\n" + .. COLOR_BLUE + .. " i" + .. COLOR_YELLOW + .. "(nspect) " + .. COLOR_BLUE + .. "[index]" + .. GREEN_CARET + .. "move to a specific stack frame\n" + .. COLOR_BLUE + .. " w" + .. COLOR_YELLOW + .. "(here) " + .. COLOR_BLUE + .. "[line count]" + .. GREEN_CARET + .. "print source code around the current line\n" + .. COLOR_BLUE + .. " e" + .. COLOR_YELLOW + .. "(val) " + .. COLOR_BLUE + .. "[statement]" + .. GREEN_CARET + .. "execute the statement\n" + .. COLOR_BLUE + .. " p" + .. COLOR_YELLOW + .. "(rint) " + .. COLOR_BLUE + .. "[expression]" + .. GREEN_CARET + .. "execute the expression and print the result\n" + .. COLOR_BLUE + .. " t" + .. COLOR_YELLOW + .. "(race)" + .. GREEN_CARET + .. "print the stack trace\n" + .. COLOR_BLUE + .. " l" + .. COLOR_YELLOW + .. "(ocals)" + .. GREEN_CARET + .. "print the function arguments, locals and upvalues.\n" + .. COLOR_BLUE + .. " h" + .. COLOR_YELLOW + .. "(elp)" + .. GREEN_CARET + .. "print this message\n" + .. COLOR_BLUE + .. " q" + .. COLOR_YELLOW + .. "(uit)" + .. GREEN_CARET + .. "halt execution\n" + ) + return false +end + +local last_cmd = false + +local commands = { + ["^c$"] = function() + return true + end, + ["^s$"] = cmd_step, + ["^n$"] = cmd_next, + ["^f$"] = cmd_finish, + ["^p%s+(.*)$"] = cmd_print, + ["^e%s+(.*)$"] = cmd_eval, + ["^u$"] = cmd_up, + ["^d$"] = cmd_down, + ["i%s*(%d+)"] = cmd_inspect, + ["^w%s*(%d*)$"] = cmd_where, + ["^t$"] = cmd_trace, + ["^l$"] = cmd_locals, + ["^h$"] = cmd_help, + ["^q$"] = function() + dbg.exit(0) + return true + end, +} + +local function match_command(line) + for pat, func in pairs(commands) do + -- Return the matching command and capture argument. + if line:find(pat) then + return func, line:match(pat) + end + end +end + +-- Run a command line +-- Returns true if the REPL should exit and the hook function factory +local function run_command(line) + -- GDB/LLDB exit on ctrl-d + if line == nil then + dbg.exit(1) + return true + end + + -- Re-execute the last command if you press return. + if line == "" then + line = last_cmd or "h" + end + + local command, command_arg = match_command(line) + if command then + last_cmd = line + -- unpack({...}) prevents tail call elimination so the stack frame indices are predictable. + return unpack { command(command_arg) } + elseif dbg.auto_eval then + return unpack { cmd_eval(line) } + else + dbg_writeln( + COLOR_RED + .. "Error:" + .. COLOR_RESET + .. " command '%s' not recognized.\nType 'h' and press return for a command list.", + line + ) + return false + end +end + +repl = function(reason) + -- Skip frames without source info. + while not frame_has_line(debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL - 3)) do + stack_inspect_offset = stack_inspect_offset + 1 + end + + local info = debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL - 3) + reason = reason and (COLOR_YELLOW .. "break via " .. COLOR_RED .. reason .. GREEN_CARET) or "" + dbg_writeln(reason .. format_stack_frame_info(info)) + + if tonumber(dbg.auto_where) then + where(info, dbg.auto_where) + end + + repeat + local success, done, hook = pcall(run_command, dbg.read(COLOR_RED .. "debugger.lua> " .. COLOR_RESET)) + if success then + debug.sethook(hook and hook(0), "crl") + else + local message = COLOR_RED .. "INTERNAL DEBUGGER.LUA ERROR. ABORTING\n:" .. COLOR_RESET .. " " .. done + dbg_writeln(message) + error(message) + end + until done +end + +-- Make the debugger object callable like a function. +dbg = setmetatable({}, { + __call = function(_, condition, top_offset, source) + if condition then + return + end + + top_offset = (top_offset or 0) + stack_inspect_offset = top_offset + stack_top = top_offset + + debug.sethook(hook_next(1, source or "dbg()"), "crl") + return + end, +}) + +-- Expose the debugger's IO functions. +dbg.read = dbg_read +dbg.write = dbg_write +dbg.shorten_path = function(path) + return path +end +dbg.exit = function(err) + os.exit(err) +end + +dbg.writeln = dbg_writeln + +dbg.pretty_depth = 3 +dbg.pretty = pretty +dbg.pp = function(value, depth) + dbg_writeln(dbg.pretty(value, depth)) +end + +dbg.auto_where = false +dbg.auto_eval = false + +local lua_error, lua_assert = error, assert + +-- Works like error(), but invokes the debugger. +function dbg.error(err, level) + level = level or 1 + dbg_writeln(COLOR_RED .. "ERROR: " .. COLOR_RESET .. dbg.pretty(err)) + dbg(false, level, "dbg.error()") + + lua_error(err, level) +end + +-- Works like assert(), but invokes the debugger on a failure. +function dbg.assert(condition, message) + message = message or "assertion failed!" + if not condition then + dbg_writeln(COLOR_RED .. "ERROR: " .. COLOR_RESET .. message) + dbg(false, 1, "dbg.assert()") + end + + return lua_assert(condition, message) +end + +-- Works like pcall(), but invokes the debugger on an error. +function dbg.call(f, ...) + return xpcall(f, function(err) + dbg_writeln(COLOR_RED .. "ERROR: " .. COLOR_RESET .. dbg.pretty(err)) + dbg(false, 1, "dbg.call()") + + return err + end, ...) +end + +-- Error message handler that can be used with lua_pcall(). +function dbg.msgh(...) + if debug.getinfo(2) then + dbg_writeln(COLOR_RED .. "ERROR: " .. COLOR_RESET .. dbg.pretty(...)) + dbg(false, 1, "dbg.msgh()") + else + dbg_writeln( + COLOR_RED + .. "debugger.lua: " + .. COLOR_RESET + .. "Error did not occur in Lua code. Execution will continue after dbg_pcall()." + ) + end + + return ... +end + +-- Assume stdin/out are TTYs unless we can use LuaJIT's FFI to properly check them. +local stdin_isatty = true +local stdout_isatty = true + +-- Conditionally enable the LuaJIT FFI. +local ffi = (jit and require "ffi") +if ffi then + ffi.cdef [[ + int isatty(int); // Unix + int _isatty(int); // Windows + void free(void *ptr); + + char *readline(const char *); + int add_history(const char *); + ]] + + local function get_func_or_nil(sym) + local success, func = pcall(function() + return ffi.C[sym] + end) + return success and func or nil + end + + local isatty = get_func_or_nil "isatty" or get_func_or_nil "_isatty" or (ffi.load "ucrtbase")["_isatty"] + stdin_isatty = isatty(0) + stdout_isatty = isatty(1) +end + +-- Conditionally enable color support. +local color_maybe_supported = (stdout_isatty and os.getenv "TERM" and os.getenv "TERM" ~= "dumb") +if color_maybe_supported and not os.getenv "DBG_NOCOLOR" then + COLOR_GRAY = string.char(27) .. "[90m" + COLOR_RED = string.char(27) .. "[91m" + COLOR_BLUE = string.char(27) .. "[94m" + COLOR_YELLOW = string.char(27) .. "[33m" + COLOR_RESET = string.char(27) .. "[0m" + GREEN_CARET = string.char(27) .. "[92m => " .. COLOR_RESET +end + +if stdin_isatty and not os.getenv "DBG_NOREADLINE" then + pcall(function() + local linenoise = require "linenoise" + + -- Load command history from ~/.lua_history + local hist_path = os.getenv "HOME" .. "/.lua_history" + linenoise.historyload(hist_path) + linenoise.historysetmaxlen(50) + + local function autocomplete(env, input, matches) + for name, _ in pairs(env) do + if name:match("^" .. input .. ".*") then + linenoise.addcompletion(matches, name) + end + end + end + + -- Auto-completion for locals and globals + linenoise.setcompletion(function(matches, input) + -- First, check the locals and upvalues. + local env = local_bindings(1, true) + autocomplete(env, input, matches) + + -- Then, check the implicit environment. + env = getmetatable(env).__index + autocomplete(env, input, matches) + end) + + dbg.read = function(prompt) + local str = linenoise.linenoise(prompt) + if str and not str:match "^%s*$" then + linenoise.historyadd(str) + linenoise.historysave(hist_path) + end + return str + end + dbg_writeln(COLOR_YELLOW .. "debugger.lua: " .. COLOR_RESET .. "Linenoise support enabled.") + end) + + -- Conditionally enable LuaJIT readline support. + pcall(function() + if dbg.read == dbg_read and ffi then + local readline = ffi.load "readline" + dbg.read = function(prompt) + local cstr = readline.readline(prompt) + if cstr ~= nil then + local str = ffi.string(cstr) + if string.match(str, "[^%s]+") then + readline.add_history(cstr) + end + + ffi.C.free(cstr) + return str + else + return nil + end + end + dbg_writeln(COLOR_YELLOW .. "debugger.lua: " .. COLOR_RESET .. "Readline support enabled.") + end + end) +end + +-- Detect Lua version. +if jit then -- LuaJIT + LUA_JIT_SETLOCAL_WORKAROUND = -1 + dbg_writeln(COLOR_YELLOW .. "debugger.lua: " .. COLOR_RESET .. "Loaded for " .. jit.version) +elseif "Lua 5.1" <= _VERSION and _VERSION <= "Lua 5.4" then + dbg_writeln(COLOR_YELLOW .. "debugger.lua: " .. COLOR_RESET .. "Loaded for " .. _VERSION) +else + dbg_writeln(COLOR_YELLOW .. "debugger.lua: " .. COLOR_RESET .. "Not tested against " .. _VERSION) + dbg_writeln "Please send me feedback!" +end + +return dbg |