From 36b99bf8a2f0bfb73f95f648566084009249c67c Mon Sep 17 00:00:00 2001 From: Joao Matos Date: Fri, 11 Oct 2024 17:14:50 +0100 Subject: [PATCH] Add interactive debugger. Adds a new interactive debugger based on [debugger.lua](https://github.com/slembcke/debugger.lua). Premake already contains some remove debugger integration (MobDebug), however this PR introduces an integrated debugger, any call to `dbg()` will launch the user into an interactive GDB-like TUI shell to allow interactive debugging on the spot. --- contrib/debugger.lua/LICENSE | 19 + contrib/debugger.lua/README.md | 140 ++++++ contrib/debugger.lua/debugger.lua | 669 +++++++++++++++++++++++++ contrib/debugger.lua/debugger_lua.h | 154 ++++++ contrib/debugger.lua/make_c_header.lua | 225 +++++++++ premake5.lua | 3 + src/host/premake.c | 9 + 7 files changed, 1219 insertions(+) create mode 100644 contrib/debugger.lua/LICENSE create mode 100644 contrib/debugger.lua/README.md create mode 100644 contrib/debugger.lua/debugger.lua create mode 100644 contrib/debugger.lua/debugger_lua.h create mode 100644 contrib/debugger.lua/make_c_header.lua diff --git a/contrib/debugger.lua/LICENSE b/contrib/debugger.lua/LICENSE new file mode 100644 index 0000000000..c38113b838 --- /dev/null +++ b/contrib/debugger.lua/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2024 Scott Lembcke and Howling Moon Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/contrib/debugger.lua/README.md b/contrib/debugger.lua/README.md new file mode 100644 index 0000000000..769f4ea7f3 --- /dev/null +++ b/contrib/debugger.lua/README.md @@ -0,0 +1,140 @@ +🐞 debugger.lua 🌖 += + +A simple, embedabble debugger for Lua 5.x, and LuaJIT 2.x. + +![ExampleLog](https://raw.githubusercontent.com/slembcke/debugger.lua/ec29cc13224750d109383c949950d7cafd6fcbdf/debugger_lua.png) + +debugger.lua is a simple, single file, pure Lua debugger that is easy to integrate with any project. The lua-users wiki lists a [number of debuggers](http://lua-users.org/wiki/DebuggingLuaCode). clidebugger was closest to what I was looking for, but I ran into several compatibility issues, and the rest are pretty big libraries with a whole lot of dependencies. I just wanted something simple to integrate that would work through stdin/stdout. I also decided that it sounded fun to try and make my own! + +✅ Features +- + +- Trivial to "install". Can be integrated as a single .lua _or_ .h file. +- The regular assortment of commands you'd expect from a debugger: continue, step, next, finish, print/eval expression, move up/down the stack, backtrace, print locals, inline help. +- Evaluate expressions, call functions interactively, and get/set variables. +- Pretty printed output so you see `{1 = 3, "a" = 5}` instead of `table: 0x10010cfa0` +- Speed! The debugger hooks are only set during the step/next/finish commands. +- Conditional, assert-style breakpoints. +- Colored output and line editing support when possible. +- Drop in replacements for Lua's `assert()`, `error()`, and `pcall()` functions that trigger the debugger. +- When using the C API, `dbg_call()` works as a drop-in replacement for `lua_pcall()`. +- IO can easily be remapped to a socket or window by overwriting the `dbg.write()` and `dbg.read()` functions. +- Permissive MIT license. + +🙌 Easy to use from C too! +- + +debugger.lua can be easily integrated into an embedded project with just a .h file. First though, you'll need to run `lua make_c_header.lua`. This generates debugger_lua.h by inserting the lua code into a template .h file. + +```c +// You need to define this in one of your C files. (and only one!) +#define DEBUGGER_LUA_IMPLEMENTATION +#include "debugger_lua.h" + +int main(int argc, char **argv){ + // Do normal Lua init stuff. + lua_State *lua = luaL_newstate(); + luaL_openlibs(lua); + + // Load up the debugger module as "debugger". + // Also stores it in a global variable "dbg". + // Use dbg_setup() to change these or use custom I/O. + dbg_setup_default(lua); + + // Load some buggy Lua code. + luaL_loadstring(lua, + "local num = 1 \n" + "local str = 'one' \n" + "local res = num + str \n" + ); + + // Run it in the debugger. This function works just like lua_pcall() otherwise. + // Note that setting your own custom message handler disables the debugger. + if(dbg_pcall(lua, 0, 0, 0)){ + fprintf(stderr, "Lua Error: %s\n", lua_tostring(lua, -1)); + } +} +``` + +Now in your Lua code you can access `dbg` or use `require 'debugger'`. + +🐝 Debugger Commands: +- + +If you have used other CLI debuggers, debugger.lua shouldn't be surprising. I didn't make a fancy parser, so the commands are just single letters. Since the debugger is pretty simple there are only a small handful of commands anwyay. + + [return] - re-run last command + c(ontinue) - contiue execution + s(tep) - step forward by one line (into functions) + n(ext) - step forward by one line (skipping over functions) + p(rint) [expression] - execute the expression and print the result + f(inish) - step forward until exiting the current function + u(p) - move up the stack by one frame + d(own) - move down the stack by one frame + w(here) [line count] - print source code around the current line + t(race) - print the stack trace + l(ocals) - print the function arguments, locals and upvalues. + h(elp) - print this message + q(uit) - halt execution + +If you've never used a command line debugger before, start a nice warm cozy fire, run tutorial.lua, and open it up in your favorite editor so you can follow along. + +🦋 Debugger API +- + +There are several overloadable functions you can use to customize debugger.lua. +* `dbg.read(prompt)` - Show the prompt and block for user input. (Defaults to read from stdin) +* `dbg.write(str)` - Write a string to the output. (Defaults to write to stdout) +* `dbg.shorten_path(path)` - Return a shortened version of a path. (Defaults to simply return `path`) +* `dbg.exit(err)` - Stop debugging. (Defaults to `os.exit(err)`) +* `dbg.pretty(obj)` - Output a pretty print string for an object. (Defaults to a reasonable version using __tostring metamethods and such) + +Using these you can customize the debugger to work in your environment. For instance, you can divert the I/O over a network socket or to a GUI window. + +There are also some goodies you can use to make debugging easier. +* `dbg.writeln(format, ...)` - Basically the same as `dbg.write(string.format(format.."\n", ...))` +* `dbg.pretty_depth = int` - Set how deep `dbg.pretty()` formats tables. +* `dbg.pretty(obj)` - Will return a pretty print string of an object. +* `dbg.pp(obj)` - Basically the same as `dbg.writeln(dbg.pretty(obj))` +* `dbg.auto_where = int_or_false` - Set the where command to run automatically when the active line changes. The value is the number of context lines. +* `dbg.error(error, [level])` - Drop in replacement for `error()` that breaks in the debugger. +* `dbg.assert(error, [message])` - Drop in replacement for `assert()` that breaks in the debugger. +* `dbg.call(f, ...)` - Drop in replacement for `pcall()` that breaks in the debugger. + +🪲 Environment Variables: +- + +Want to disable ANSI color support or disable GNU readline? Set the `DBG_NOCOLOR` and/or `DBG_NOREADLINE` environment variables. + +🕷️ Known Issues: +- + +- Lua 5.1 lacks the API to access varargs. The workaround is to do something like `local args = {...}` and then use `unpack(args)` when you want to access them. In Lua 5.2+ and LuaJIT, you can simply use `...` in your expressions with the print command. +- You can't add breakpoints to a running program or remove them. Currently the only way to set them is by explicitly calling the `dbg()` function explicitly in your code. (This is sort of by design and sort of because it's difficult/slow otherwise.) +- Different interpreters (and versions) print out slightly different stack trace information. +- Tail calls are handled silghtly differently in different interpreters. You may find that 1.) stepping into a function that does nothing but a tail call steps you into the tail called function. 2.) The interpreter gives you the wrong name of a tail called function (watch the line numbers). 3.) Stepping out of a tail called function also steps out of the function that performed the tail call. Mostly this is never a problem, but it is a little confusing if you don't know what is going on. +- Coroutine support has not been tested extensively yet, and Lua vs. LuaJIT handle them differently anyway. -_- + +🪰 License: +- + + Copyright (c) 2024 Scott Lembcke and Howling Moon Software + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. diff --git a/contrib/debugger.lua/debugger.lua b/contrib/debugger.lua/debugger.lua new file mode 100644 index 0000000000..c2464c24a5 --- /dev/null +++ b/contrib/debugger.lua/debugger.lua @@ -0,0 +1,669 @@ +-- 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 "") + 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 "").."\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 = "" 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.." "..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 diff --git a/contrib/debugger.lua/debugger_lua.h b/contrib/debugger.lua/debugger_lua.h new file mode 100644 index 0000000000..4f91ea2714 --- /dev/null +++ b/contrib/debugger.lua/debugger_lua.h @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2024 Scott Lembcke and Howling Moon Software + +/* + Using debugger.lua from C code is pretty straightforward. + Basically you just need to call one of the setup functions to make the debugger available. + Then you can reference the debugger in your Lua code as normal. + If you want to wrap the lua code from your C entrypoints, you can use + dbg_pcall() or dbg_dofile() instead. + + That's it!! + + #include + #include + #include + #include + + #define DEBUGGER_LUA_IMPLEMENTATION + #include "debugger_lua.h" + + int main(int argc, char **argv){ + lua_State *lua = luaL_newstate(); + luaL_openlibs(lua); + + // This defines a module named 'debugger' which is assigned to a global named 'dbg'. + // If you want to change these values or redirect the I/O, then use dbg_setup() instead. + dbg_setup_default(lua); + + luaL_loadstring(lua, + "local num = 1\n" + "local str = 'one'\n" + "local res = num + str\n" + ); + + // Call into the lua code, and catch any unhandled errors in the debugger. + if(dbg_pcall(lua, 0, 0, 0)){ + fprintf(stderr, "Lua Error: %s\n", lua_tostring(lua, -1)); + } + } +*/ + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct lua_State lua_State; +typedef int (*lua_CFunction)(lua_State *L); + +// This function must be called before calling dbg_pcall() to set up the debugger module. +// 'name' must be the name of the module to register the debugger as. (to use with require 'module') +// 'globalName' can either be NULL or a global variable name to assign the debugger to. (I use "dbg") +// 'readFunc' is a lua_CFunction that returns a line of input when called. Pass NULL if you want to read from stdin. +// 'writeFunc' is a lua_CFunction that takes a single string as an argument. Pass NULL if you want to write to stdout. +void dbg_setup(lua_State *lua, const char *name, const char *globalName, lua_CFunction readFunc, lua_CFunction writeFunc); + +// Same as 'dbg_setup(lua, "debugger", "dbg", NULL, NULL)' +void dbg_setup_default(lua_State *lua); + +// Drop in replacement for lua_pcall() that attaches the debugger on an error if 'msgh' is 0. +int dbg_pcall(lua_State *lua, int nargs, int nresults, int msgh); + +// Drop in replacement for luaL_dofile() +#define dbg_dofile(lua, filename) (luaL_loadfile(lua, filename) || dbg_pcall(lua, 0, LUA_MULTRET, 0)) + +#ifdef DEBUGGER_LUA_IMPLEMENTATION + +#include +#include +#include + +static const char DEBUGGER_SRC[] = "-- SPDX-License-Identifier: MIT\n-- Copyright (c) 2024 Scott Lembcke and Howling Moon Software\n\nlocal dbg\n\n-- Use ANSI color codes in the prompt by default.\nlocal COLOR_GRAY = \"\"\nlocal COLOR_RED = \"\"\nlocal COLOR_BLUE = \"\"\nlocal COLOR_YELLOW = \"\"\nlocal COLOR_RESET = \"\"\nlocal GREEN_CARET = \" => \"\n\nlocal function pretty(obj, max_depth)\n\tif max_depth == nil then max_depth = dbg.pretty_depth end\n\t\n\t-- Returns true if a table has a __tostring metamethod.\n\tlocal function coerceable(tbl)\n\t\tlocal meta = getmetatable(tbl)\n\t\treturn (meta and meta.__tostring)\n\tend\n\t\n\tlocal function recurse(obj, depth)\n\t\tif type(obj) == \"string\" then\n\t\t\t-- Dump the string so that escape sequences are printed.\n\t\t\treturn string.format(\"%q\", obj)\n\t\telseif type(obj) == \"table\" and depth < max_depth and not coerceable(obj) then\n\t\t\tlocal str = \"{\"\n\t\t\t\n\t\t\tfor k, v in pairs(obj) do\n\t\t\t\tlocal pair = pretty(k, 0)..\" = \"..recurse(v, depth + 1)\n\t\t\t\tstr = str..(str == \"{\" and pair or \", \"..pair)\n\t\t\tend\n\t\t\t\n\t\t\treturn str..\"}\"\n\t\telse\n\t\t\t-- tostring() can fail if there is an error in a __tostring metamethod.\n\t\t\tlocal success, value = pcall(function() return tostring(obj) end)\n\t\t\treturn (success and value or \"\")\n\t\tend\n\tend\n\t\n\treturn recurse(obj, 0)\nend\n\n-- The stack level that cmd_* functions use to access locals or info\n-- The structure of the code very carefully ensures this.\nlocal CMD_STACK_LEVEL = 6\n\n-- Location of the top of the stack outside of the debugger.\n-- Adjusted by some debugger entrypoints.\nlocal stack_top = 0\n\n-- The current stack frame index.\n-- Changed using the up/down commands\nlocal stack_inspect_offset = 0\n\n-- LuaJIT has an off by one bug when setting local variables.\nlocal LUA_JIT_SETLOCAL_WORKAROUND = 0\n\n-- Default dbg.read function\nlocal function dbg_read(prompt)\n\tdbg.write(prompt)\n\tio.flush()\n\treturn io.read()\nend\n\n-- Default dbg.write function\nlocal function dbg_write(str)\n\tio.write(str)\nend\n\nlocal function dbg_writeln(str, ...)\n\tif select(\"#\", ...) == 0 then\n\t\tdbg.write((str or \"\")..\"\\n\")\n\telse\n\t\tdbg.write(string.format(str..\"\\n\", ...))\n\tend\nend\n\nlocal function format_loc(file, line) return COLOR_BLUE..file..COLOR_RESET..\":\"..COLOR_YELLOW..line..COLOR_RESET end\nlocal function format_stack_frame_info(info)\n\tlocal filename = info.source:match(\"@(.*)\")\n\tlocal source = filename and dbg.shorten_path(filename) or info.short_src\n\tlocal namewhat = (info.namewhat == \"\" and \"chunk at\" or info.namewhat)\n\tlocal name = (info.name and \"'\"..COLOR_BLUE..info.name..COLOR_RESET..\"'\" or format_loc(source, info.linedefined))\n\treturn format_loc(source, info.currentline)..\" in \"..namewhat..\" \"..name\nend\n\nlocal repl\n\n-- Return false for stack frames without source,\n-- which includes C frames, Lua bytecode, and `loadstring` functions\nlocal function frame_has_line(info) return info.currentline >= 0 end\n\nlocal function hook_factory(repl_threshold)\n\treturn function(offset, reason)\n\t\treturn function(event, _)\n\t\t\t-- Skip events that don't have line information.\n\t\t\tif not frame_has_line(debug.getinfo(2)) then return end\n\t\t\t\n\t\t\t-- Tail calls are specifically ignored since they also will have tail returns to balance out.\n\t\t\tif event == \"call\" then\n\t\t\t\toffset = offset + 1\n\t\t\telseif event == \"return\" and offset > repl_threshold then\n\t\t\t\toffset = offset - 1\n\t\t\telseif event == \"line\" and offset <= repl_threshold then\n\t\t\t\trepl(reason)\n\t\t\tend\n\t\tend\n\tend\nend\n\nlocal hook_step = hook_factory(1)\nlocal hook_next = hook_factory(0)\nlocal hook_finish = hook_factory(-1)\n\n-- Create a table of all the locally accessible variables.\n-- Globals are not included when running the locals command, but are when running the print command.\nlocal function local_bindings(offset, include_globals)\n\tlocal level = offset + stack_inspect_offset + CMD_STACK_LEVEL\n\tlocal func = debug.getinfo(level).func\n\tlocal bindings = {}\n\t\n\t-- Retrieve the upvalues\n\tdo local i = 1; while true do\n\t\tlocal name, value = debug.getupvalue(func, i)\n\t\tif not name then break end\n\t\tbindings[name] = value\n\t\ti = i + 1\n\tend end\n\t\n\t-- Retrieve the locals (overwriting any upvalues)\n\tdo local i = 1; while true do\n\t\tlocal name, value = debug.getlocal(level, i)\n\t\tif not name then break end\n\t\tbindings[name] = value\n\t\ti = i + 1\n\tend end\n\t\n\t-- Retrieve the varargs (works in Lua 5.2 and LuaJIT)\n\tlocal varargs = {}\n\tdo local i = 1; while true do\n\t\tlocal name, value = debug.getlocal(level, -i)\n\t\tif not name then break end\n\t\tvarargs[i] = value\n\t\ti = i + 1\n\tend end\n\tif #varargs > 0 then bindings[\"...\"] = varargs end\n\t\n\tif include_globals then\n\t\t-- In Lua 5.2, you have to get the environment table from the function's locals.\n\t\tlocal env = (_VERSION <= \"Lua 5.1\" and getfenv(func) or bindings._ENV)\n\t\treturn setmetatable(bindings, {__index = env or _G})\n\telse\n\t\treturn bindings\n\tend\nend\n\n-- Used as a __newindex metamethod to modify variables in cmd_eval().\nlocal function mutate_bindings(_, name, value)\n\tlocal FUNC_STACK_OFFSET = 3 -- Stack depth of this function.\n\tlocal level = stack_inspect_offset + FUNC_STACK_OFFSET + CMD_STACK_LEVEL\n\t\n\t-- Set a local.\n\tdo local i = 1; repeat\n\t\tlocal var = debug.getlocal(level, i)\n\t\tif name == var then\n\t\t\tdbg_writeln(COLOR_YELLOW..\"debugger.lua\"..GREEN_CARET..\"Set local variable \"..COLOR_BLUE..name..COLOR_RESET)\n\t\t\treturn debug.setlocal(level + LUA_JIT_SETLOCAL_WORKAROUND, i, value)\n\t\tend\n\t\ti = i + 1\n\tuntil var == nil end\n\t\n\t-- Set an upvalue.\n\tlocal func = debug.getinfo(level).func\n\tdo local i = 1; repeat\n\t\tlocal var = debug.getupvalue(func, i)\n\t\tif name == var then\n\t\t\tdbg_writeln(COLOR_YELLOW..\"debugger.lua\"..GREEN_CARET..\"Set upvalue \"..COLOR_BLUE..name..COLOR_RESET)\n\t\t\treturn debug.setupvalue(func, i, value)\n\t\tend\n\t\ti = i + 1\n\tuntil var == nil end\n\t\n\t-- Set a global.\n\tdbg_writeln(COLOR_YELLOW..\"debugger.lua\"..GREEN_CARET..\"Set global variable \"..COLOR_BLUE..name..COLOR_RESET)\n\t_G[name] = value\nend\n\n-- Compile an expression with the given variable bindings.\nlocal function compile_chunk(block, env)\n\tlocal source = \"debugger.lua REPL\"\n\tlocal chunk = nil\n\t\n\tif _VERSION <= \"Lua 5.1\" then\n\t\tchunk = loadstring(block, source)\n\t\tif chunk then setfenv(chunk, env) end\n\telse\n\t\t-- The Lua 5.2 way is a bit cleaner\n\t\tchunk = load(block, source, \"t\", env)\n\tend\n\t\n\tif not chunk then dbg_writeln(COLOR_RED..\"Error: Could not compile block:\\n\"..COLOR_RESET..block) end\n\treturn chunk\nend\n\nlocal SOURCE_CACHE = {}\n\nlocal function where(info, context_lines)\n\tlocal source = SOURCE_CACHE[info.source]\n\tif not source then\n\t\tsource = {}\n\t\tlocal filename = info.source:match(\"@(.*)\")\n\t\tif filename then\n\t\t\tpcall(function() for line in io.lines(filename) do table.insert(source, line) end end)\n\t\telseif info.source then\n\t\t\tfor line in info.source:gmatch(\"(.-)\\n\") do table.insert(source, line) end\n\t\tend\n\t\tSOURCE_CACHE[info.source] = source\n\tend\n\t\n\tif source and source[info.currentline] then\n\t\tfor i = info.currentline - context_lines, info.currentline + context_lines do\n\t\t\tlocal tab_or_caret = (i == info.currentline and GREEN_CARET or \" \")\n\t\t\tlocal line = source[i]\n\t\t\tif line then dbg_writeln(COLOR_GRAY..\"% 4d\"..tab_or_caret..\"%s\", i, line) end\n\t\tend\n\telse\n\t\tdbg_writeln(COLOR_RED..\"Error: Source not available for \"..COLOR_BLUE..info.short_src);\n\tend\n\t\n\treturn false\nend\n\n-- Wee version differences\nlocal unpack = unpack or table.unpack\nlocal pack = function(...) return {n = select(\"#\", ...), ...} end\n\nlocal function cmd_step()\n\tstack_inspect_offset = stack_top\n\treturn true, hook_step\nend\n\nlocal function cmd_next()\n\tstack_inspect_offset = stack_top\n\treturn true, hook_next\nend\n\nlocal function cmd_finish()\n\tlocal offset = stack_top - stack_inspect_offset\n\tstack_inspect_offset = stack_top\n\treturn true, offset < 0 and hook_factory(offset - 1) or hook_finish\nend\n\nlocal function cmd_print(expr)\n\tlocal env = local_bindings(1, true)\n\tlocal chunk = compile_chunk(\"return \"..expr, env)\n\tif chunk == nil then return false end\n\t\n\t-- Call the chunk and collect the results.\n\tlocal results = pack(pcall(chunk, unpack(rawget(env, \"...\") or {})))\n\t\n\t-- The first result is the pcall error.\n\tif not results[1] then\n\t\tdbg_writeln(COLOR_RED..\"Error:\"..COLOR_RESET..\" \"..results[2])\n\telse\n\t\tlocal output = \"\"\n\t\tfor i = 2, results.n do\n\t\t\toutput = output..(i ~= 2 and \", \" or \"\")..dbg.pretty(results[i])\n\t\tend\n\t\t\n\t\tif output == \"\" then output = \"\" end\n\t\tdbg_writeln(COLOR_BLUE..expr.. GREEN_CARET..output)\n\tend\n\t\n\treturn false\nend\n\nlocal function cmd_eval(code)\n\tlocal env = local_bindings(1, true)\n\tlocal mutable_env = setmetatable({}, {\n\t\t__index = env,\n\t\t__newindex = mutate_bindings,\n\t})\n\t\n\tlocal chunk = compile_chunk(code, mutable_env)\n\tif chunk == nil then return false end\n\t\n\t-- Call the chunk and collect the results.\n\tlocal success, err = pcall(chunk, unpack(rawget(env, \"...\") or {}))\n\tif not success then\n\t\tdbg_writeln(COLOR_RED..\"Error:\"..COLOR_RESET..\" \"..tostring(err))\n\tend\n\t\n\treturn false\nend\n\nlocal function cmd_down()\n\tlocal offset = stack_inspect_offset\n\tlocal info\n\t\n\trepeat -- Find the next frame with a file.\n\t\toffset = offset + 1\n\t\tinfo = debug.getinfo(offset + CMD_STACK_LEVEL)\n\tuntil not info or frame_has_line(info)\n\t\n\tif info then\n\t\tstack_inspect_offset = offset\n\t\tdbg_writeln(\"Inspecting frame: \"..format_stack_frame_info(info))\n\t\tif tonumber(dbg.auto_where) then where(info, dbg.auto_where) end\n\telse\n\t\tinfo = debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL)\n\t\tdbg_writeln(\"Already at the bottom of the stack.\")\n\tend\n\t\n\treturn false\nend\n\n" +"local function cmd_up()\n\tlocal offset = stack_inspect_offset\n\tlocal info\n\t\n\trepeat -- Find the next frame with a file.\n\t\toffset = offset - 1\n\t\tif offset < stack_top then info = nil; break end\n\t\tinfo = debug.getinfo(offset + CMD_STACK_LEVEL)\n\tuntil frame_has_line(info)\n\t\n\tif info then\n\t\tstack_inspect_offset = offset\n\t\tdbg_writeln(\"Inspecting frame: \"..format_stack_frame_info(info))\n\t\tif tonumber(dbg.auto_where) then where(info, dbg.auto_where) end\n\telse\n\t\tinfo = debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL)\n\t\tdbg_writeln(\"Already at the top of the stack.\")\n\tend\n\t\n\treturn false\nend\n\nlocal function cmd_inspect(offset)\n\toffset = stack_top + tonumber(offset)\n\tlocal info = debug.getinfo(offset + CMD_STACK_LEVEL)\n\tif info then\n\t\tstack_inspect_offset = offset\n\t\tdbg.writeln(\"Inspecting frame: \"..format_stack_frame_info(info))\n\telse\n\t\tdbg.writeln(COLOR_RED..\"ERROR: \"..COLOR_BLUE..\"Invalid stack frame index.\"..COLOR_RESET)\n\tend\nend\n\nlocal function cmd_where(context_lines)\n\tlocal info = debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL)\n\treturn (info and where(info, tonumber(context_lines) or 5))\nend\n\nlocal function cmd_trace()\n\tdbg_writeln(\"Inspecting frame %d\", stack_inspect_offset - stack_top)\n\tlocal i = 0; while true do\n\t\tlocal info = debug.getinfo(stack_top + CMD_STACK_LEVEL + i)\n\t\tif not info then break end\n\t\t\n\t\tlocal is_current_frame = (i + stack_top == stack_inspect_offset)\n\t\tlocal tab_or_caret = (is_current_frame and GREEN_CARET or \" \")\n\t\tdbg_writeln(COLOR_GRAY..\"% 4d\"..COLOR_RESET..tab_or_caret..\"%s\", i, format_stack_frame_info(info))\n\t\ti = i + 1\n\tend\n\t\n\treturn false\nend\n\nlocal function cmd_locals()\n\tlocal bindings = local_bindings(1, false)\n\t\n\t-- Get all the variable binding names and sort them\n\tlocal keys = {}\n\tfor k, _ in pairs(bindings) do table.insert(keys, k) end\n\ttable.sort(keys)\n\t\n\tfor _, k in ipairs(keys) do\n\t\tlocal v = bindings[k]\n\t\t\n\t\t-- Skip the debugger object itself, \"(*internal)\" values, and Lua 5.2's _ENV object.\n\t\tif not rawequal(v, dbg) and k ~= \"_ENV\" and not k:match(\"%(.*%)\") then\n\t\t\tdbg_writeln(\" \"..COLOR_BLUE..k.. GREEN_CARET..dbg.pretty(v))\n\t\tend\n\tend\n\t\n\treturn false\nend\n\nlocal function cmd_help()\n\tdbg.write(\"\"\n\t\t..COLOR_BLUE..\" \"..GREEN_CARET..\"re-run last command\\n\"\n\t\t..COLOR_BLUE..\" c\"..COLOR_YELLOW..\"(ontinue)\"..GREEN_CARET..\"continue execution\\n\"\n\t\t..COLOR_BLUE..\" s\"..COLOR_YELLOW..\"(tep)\"..GREEN_CARET..\"step forward by one line (into functions)\\n\"\n\t\t..COLOR_BLUE..\" n\"..COLOR_YELLOW..\"(ext)\"..GREEN_CARET..\"step forward by one line (skipping over functions)\\n\"\n\t\t..COLOR_BLUE..\" f\"..COLOR_YELLOW..\"(inish)\"..GREEN_CARET..\"step forward until exiting the current function\\n\"\n\t\t..COLOR_BLUE..\" u\"..COLOR_YELLOW..\"(p)\"..GREEN_CARET..\"move up the stack by one frame\\n\"\n\t\t..COLOR_BLUE..\" d\"..COLOR_YELLOW..\"(own)\"..GREEN_CARET..\"move down the stack by one frame\\n\"\n\t\t..COLOR_BLUE..\" i\"..COLOR_YELLOW..\"(nspect) \"..COLOR_BLUE..\"[index]\"..GREEN_CARET..\"move to a specific stack frame\\n\"\n\t\t..COLOR_BLUE..\" w\"..COLOR_YELLOW..\"(here) \"..COLOR_BLUE..\"[line count]\"..GREEN_CARET..\"print source code around the current line\\n\"\n\t\t..COLOR_BLUE..\" e\"..COLOR_YELLOW..\"(val) \"..COLOR_BLUE..\"[statement]\"..GREEN_CARET..\"execute the statement\\n\"\n\t\t..COLOR_BLUE..\" p\"..COLOR_YELLOW..\"(rint) \"..COLOR_BLUE..\"[expression]\"..GREEN_CARET..\"execute the expression and print the result\\n\"\n\t\t..COLOR_BLUE..\" t\"..COLOR_YELLOW..\"(race)\"..GREEN_CARET..\"print the stack trace\\n\"\n\t\t..COLOR_BLUE..\" l\"..COLOR_YELLOW..\"(ocals)\"..GREEN_CARET..\"print the function arguments, locals and upvalues.\\n\"\n\t\t..COLOR_BLUE..\" h\"..COLOR_YELLOW..\"(elp)\"..GREEN_CARET..\"print this message\\n\"\n\t\t..COLOR_BLUE..\" q\"..COLOR_YELLOW..\"(uit)\"..GREEN_CARET..\"halt execution\\n\"\n\t)\n\treturn false\nend\n\nlocal last_cmd = false\n\nlocal commands = {\n\t[\"^c$\"] = function() return true end,\n\t[\"^s$\"] = cmd_step,\n\t[\"^n$\"] = cmd_next,\n\t[\"^f$\"] = cmd_finish,\n\t[\"^p%s+(.*)$\"] = cmd_print,\n\t[\"^e%s+(.*)$\"] = cmd_eval,\n\t[\"^u$\"] = cmd_up,\n\t[\"^d$\"] = cmd_down,\n\t[\"i%s*(%d+)\"] = cmd_inspect,\n\t[\"^w%s*(%d*)$\"] = cmd_where,\n\t[\"^t$\"] = cmd_trace,\n\t[\"^l$\"] = cmd_locals,\n\t[\"^h$\"] = cmd_help,\n\t[\"^q$\"] = function() dbg.exit(0); return true end,\n}\n\nlocal function match_command(line)\n\tfor pat, func in pairs(commands) do\n\t\t-- Return the matching command and capture argument.\n\t\tif line:find(pat) then return func, line:match(pat) end\n\tend\nend\n\n-- Run a command line\n-- Returns true if the REPL should exit and the hook function factory\nlocal function run_command(line)\n\t-- GDB/LLDB exit on ctrl-d\n\tif line == nil then dbg.exit(1); return true end\n\t\n\t-- Re-execute the last command if you press return.\n\tif line == \"\" then line = last_cmd or \"h\" end\n\t\n\tlocal command, command_arg = match_command(line)\n\tif command then\n\t\tlast_cmd = line\n\t\t-- unpack({...}) prevents tail call elimination so the stack frame indices are predictable.\n\t\treturn unpack({command(command_arg)})\n\telseif dbg.auto_eval then\n\t\treturn unpack({cmd_eval(line)})\n\telse\n\t\tdbg_writeln(COLOR_RED..\"Error:\"..COLOR_RESET..\" command '%s' not recognized.\\nType 'h' and press return for a command list.\", line)\n\t\treturn false\n\tend\nend\n\nrepl = function(reason)\n\t-- Skip frames without source info.\n\twhile not frame_has_line(debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL - 3)) do\n\t\tstack_inspect_offset = stack_inspect_offset + 1\n\tend\n\t\n\tlocal info = debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL - 3)\n\treason = reason and (COLOR_YELLOW..\"break via \"..COLOR_RED..reason..GREEN_CARET) or \"\"\n\tdbg_writeln(reason..format_stack_frame_info(info))\n\t\n\tif tonumber(dbg.auto_where) then where(info, dbg.auto_where) end\n\t\n\trepeat\n\t\tlocal success, done, hook = pcall(run_command, dbg.read(COLOR_RED..\"debugger.lua> \"..COLOR_RESET))\n\t\tif success then\n\t\t\tdebug.sethook(hook and hook(0), \"crl\")\n\t\telse\n\t\t\tlocal message = COLOR_RED..\"INTERNAL DEBUGGER.LUA ERROR. ABORTING\\n:\"..COLOR_RESET..\" \"..done\n\t\t\tdbg_writeln(message)\n\t\t\terror(message)\n\t\tend\n\tuntil done\nend\n\n-- Make the debugger object callable like a function.\ndbg = setmetatable({}, {\n\t__call = function(_, condition, top_offset, source)\n\t\tif condition then return end\n\t\t\n\t\ttop_offset = (top_offset or 0)\n\t\tstack_inspect_offset = top_offset\n\t\tstack_top = top_offset\n\t\t\n\t\tdebug.sethook(hook_next(1, source or \"dbg()\"), \"crl\")\n\t\treturn\n\tend,\n})\n\n-- Expose the debugger's IO functions.\ndbg.read = dbg_read\ndbg.write = dbg_write\ndbg.shorten_path = function (path) return path end\ndbg.exit = function(err) os.exit(err) end\n\ndbg.writeln = dbg_writeln\n\ndbg.pretty_depth = 3\ndbg.pretty = pretty\ndbg.pp = function(value, depth) dbg_writeln(dbg.pretty(value, depth)) end\n\ndbg.auto_where = false\ndbg.auto_eval = false\n\nlocal lua_error, lua_assert = error, assert\n\n-- Works like error(), but invokes the debugger.\nfunction dbg.error(err, level)\n\tlevel = level or 1\n\tdbg_writeln(COLOR_RED..\"ERROR: \"..COLOR_RESET..dbg.pretty(err))\n\tdbg(false, level, \"dbg.error()\")\n\t\n\tlua_error(err, level)\nend\n\n-- Works like assert(), but invokes the debugger on a failure.\nfunction dbg.assert(condition, message)\n\tmessage = message or \"assertion failed!\"\n\tif not condition then\n\t\tdbg_writeln(COLOR_RED..\"ERROR: \"..COLOR_RESET..message)\n\t\tdbg(false, 1, \"dbg.assert()\")\n\tend\n\t\n\treturn lua_assert(condition, message)\nend\n\n-- Works like pcall(), but invokes the debugger on an error.\nfunction dbg.call(f, ...)\n\treturn xpcall(f, function(err)\n\t\tdbg_writeln(COLOR_RED..\"ERROR: \"..COLOR_RESET..dbg.pretty(err))\n\t\tdbg(false, 1, \"dbg.call()\")\n\t\t\n\t\treturn err\n\tend, ...)\nend\n\n-- Error message handler that can be used with lua_pcall().\nfunction dbg.msgh(...)\n\tif debug.getinfo(2) then\n\t\tdbg_writeln(COLOR_RED..\"ERROR: \"..COLOR_RESET..dbg.pretty(...))\n\t\tdbg(false, 1, \"dbg.msgh()\")\n\telse\n\t\tdbg_writeln(COLOR_RED..\"debugger.lua: \"..COLOR_RESET..\"Error did not occur in Lua code. Execution will continue after dbg_pcall().\")\n\tend\n\t\n\treturn ...\nend\n\n-- Assume stdin/out are TTYs unless we can use LuaJIT's FFI to properly check them.\nlocal stdin_isatty = true\nlocal stdout_isatty = true\n\n-- Conditionally enable the LuaJIT FFI.\nlocal ffi = (jit and require(\"ffi\"))\nif ffi then\n\tffi.cdef[[\n\t\tint isatty(int); // Unix\n\t\tint _isatty(int); // Windows\n\t\tvoid free(void *ptr);\n\t\t\n\t\tchar *readline(const char *);\n\t\tint add_history(const char *);\n\t]]\n\t\n\tlocal function get_func_or_nil(sym)\n\t\tlocal success, func = pcall(function() return ffi.C[sym] end)\n\t\treturn success and func or nil\n\tend\n\t\n\tlocal isatty = get_func_or_nil(\"isatty\") or get_func_or_nil(\"_isatty\") or (ffi.load(\"ucrtbase\"))[\"_isatty\"]\n\tstdin_isatty = isatty(0)\n\tstdout_isatty = isatty(1)\nend\n\n-- Conditionally enable color support.\nlocal color_maybe_supported = (stdout_isatty and os.getenv(\"TERM\") and os.getenv(\"TERM\") ~= \"dumb\")\nif color_maybe_supported and not os.getenv(\"DBG_NOCOLOR\") then\n\tCOLOR_GRAY = string.char(27) .. \"[90m\"\n\tCOLOR_RED = string.char(27) .. \"[91m\"\n\tCOLOR_BLUE = string.char(27) .. \"[94m\"\n\tCOLOR_YELLOW = string.char(27) .. \"[33m\"\n\tCOLOR_RESET = string.char(27) .. \"[0m\"\n\tGREEN_CARET = string.char(27) .. \"[92m => \"..COLOR_RESET\nend\n\nif stdin_isatty and not os.getenv(\"DBG_NOREADLINE\") then\n\tpcall(function()\n\t\tlocal linenoise = require 'linenoise'\n\t\t\n\t\t-- Load command history from ~/.lua_history\n\t\tlocal hist_path = os.getenv('HOME') .. '/.lua_history'\n\t\tlinenoise.historyload(hist_path)\n\t\tlinenoise.historysetmaxlen(50)\n\t\t\n\t\tlocal function autocomplete(env, input, matches)\n\t\t\tfor name, _ in pairs(env) do\n\t\t\t\tif name:match('^' .. input .. '.*') then\n\t\t\t\t\tlinenoise.addcompletion(matches, name)\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\t\t\n\t\t-- Auto-completion for locals and globals\n\t\tlinenoise.setcompletion(function(matches, input)\n\t\t\t-- First, check the locals and upvalues.\n\t\t\tlocal env = local_bindings(1, true)\n\t\t\tautocomplete(env, input, matches)\n\t\t\t\n\t\t\t-- Then, check the implicit environment.\n\t\t\tenv = getmetatable(env).__index\n\t\t\tautocomplete(env, input, matches)\n\t\tend)\n\t\t\n\t\tdbg.read = function(prompt)\n\t\t\tlocal str = linenoise.linenoise(prompt)\n\t\t\tif str and not str:match \"^%s*$\" then\n\t\t\t\tlinenoise.historyadd(str)\n\t\t\t\tlinenoise.historysave(hist_path)\n\t\t\tend\n\t\t\treturn str\n\t\tend\n\t\tdbg_writeln(COLOR_YELLOW..\"debugger.lua: \"..COLOR_RESET..\"Linenoise support enabled.\")\n\tend)\n\t\n\t-- Conditionally enable LuaJIT readline support.\n\tpcall(function()\n\t\tif dbg.read == dbg_read and ffi then\n\t\t\tlocal readline = ffi.load(\"readline\")\n\t\t\tdbg.read = function(prompt)\n\t\t\t\tlocal cstr = readline.readline(prompt)\n\t\t\t\tif cstr ~= nil then\n\t\t\t\t\tlocal str = ffi.string(cstr)\n\t\t\t\t\tif string.match(str, \"[^%s]+\") then\n\t\t\t\t\t\treadline.add_history(cstr)\n\t\t\t\t\tend\n\n\t\t\t\t\tffi.C.free(cstr)\n\t\t\t\t\treturn str\n\t\t\t\telse\n\t\t\t\t\treturn nil\n\t\t\t\tend\n\t\t\tend\n\t\t\tdbg_writeln(COLOR_YELLOW..\"debugger.lua: \"..COLOR_RESET..\"Readline support enabled.\")\n\t\tend\n\tend)\nend\n\n-- Detect Lua version.\nif jit then -- LuaJIT\n\tLUA_JIT_SETLOCAL_WORKAROUND = -1\n\tdbg_writeln(COLOR_YELLOW..\"debugger.lua: \"..COLOR_RESET..\"Loaded for \"..jit.version)\nelseif \"Lua 5.1\" <= _VERSION and _VERSION <= \"Lua 5.4\" then\n\tdbg_writeln(COLOR_YELLOW..\"debugger.lua: \"..COLOR_RESET..\"Loaded for \".._VERSION)\nelse\n\tdbg_writeln(COLOR_YELLOW..\"debugger.lua: \"..COLOR_RESET..\"Not tested against \".._VERSION)\n\tdbg_writeln(\"Please send me feedback!\")\nend\n\nreturn dbg\n"; + +int luaopen_debugger(lua_State *lua){ + if( + luaL_loadbufferx(lua, DEBUGGER_SRC, sizeof(DEBUGGER_SRC) - 1, "", NULL) || + lua_pcall(lua, 0, LUA_MULTRET, 0) + ) lua_error(lua); + + // Or you could load it from disk: + // if(luaL_dofile(lua, "debugger.lua")) lua_error(lua); + + return 1; +} + +static const char *MODULE_NAME = "DEBUGGER_LUA_MODULE"; +static const char *MSGH = "DEBUGGER_LUA_MSGH"; + +void dbg_setup(lua_State *lua, const char *name, const char *globalName, lua_CFunction readFunc, lua_CFunction writeFunc){ + // Check that the module name was not already defined. + lua_getfield(lua, LUA_REGISTRYINDEX, MODULE_NAME); + assert(lua_isnil(lua, -1) || strcmp(name, luaL_checkstring(lua, -1))); + lua_pop(lua, 1); + + // Push the module name into the registry. + lua_pushstring(lua, name); + lua_setfield(lua, LUA_REGISTRYINDEX, MODULE_NAME); + + // Preload the module + luaL_requiref(lua, name, luaopen_debugger, false); + + // Insert the msgh function into the registry. + lua_getfield(lua, -1, "msgh"); + lua_setfield(lua, LUA_REGISTRYINDEX, MSGH); + + if(readFunc){ + lua_pushcfunction(lua, readFunc); + lua_setfield(lua, -2, "read"); + } + + if(writeFunc){ + lua_pushcfunction(lua, writeFunc); + lua_setfield(lua, -2, "write"); + } + + if(globalName){ + lua_setglobal(lua, globalName); + } else { + lua_pop(lua, 1); + } +} + +void dbg_setup_default(lua_State *lua){ + dbg_setup(lua, "debugger", "dbg", NULL, NULL); +} + +int dbg_pcall(lua_State *lua, int nargs, int nresults, int msgh){ + // Call regular lua_pcall() if a message handler is provided. + if(msgh) return lua_pcall(lua, nargs, nresults, msgh); + + // Grab the msgh function out of the registry. + lua_getfield(lua, LUA_REGISTRYINDEX, MSGH); + if(lua_isnil(lua, -1)){ + luaL_error(lua, "Tried to call dbg_call() before calling dbg_setup()."); + } + + // Move the error handler just below the function. + msgh = lua_gettop(lua) - (1 + nargs); + lua_insert(lua, msgh); + + // Call the function. + int err = lua_pcall(lua, nargs, nresults, msgh); + + // Remove the debug handler. + lua_remove(lua, msgh); + + return err; +} + +#endif + +#ifdef __cplusplus +} +#endif diff --git a/contrib/debugger.lua/make_c_header.lua b/contrib/debugger.lua/make_c_header.lua new file mode 100644 index 0000000000..47b9b57e4c --- /dev/null +++ b/contrib/debugger.lua/make_c_header.lua @@ -0,0 +1,225 @@ +-- SPDX-License-Identifier: MIT +-- Copyright (c) 2024 Scott Lembcke and Howling Moon Software + +local template = [==[ +// SPDX-License-Identifier: MIT +// Copyright (c) 2024 Scott Lembcke and Howling Moon Software + +/* + Using debugger.lua from C code is pretty straightforward. + Basically you just need to call one of the setup functions to make the debugger available. + Then you can reference the debugger in your Lua code as normal. + If you want to wrap the lua code from your C entrypoints, you can use + dbg_pcall() or dbg_dofile() instead. + + That's it!! + + #include + #include + #include + #include + + #define DEBUGGER_LUA_IMPLEMENTATION + #include "debugger_lua.h" + + int main(int argc, char **argv){ + lua_State *lua = luaL_newstate(); + luaL_openlibs(lua); + + // This defines a module named 'debugger' which is assigned to a global named 'dbg'. + // If you want to change these values or redirect the I/O, then use dbg_setup() instead. + dbg_setup_default(lua); + + luaL_loadstring(lua, + "local num = 1\n" + "local str = 'one'\n" + "local res = num + str\n" + ); + + // Call into the lua code, and catch any unhandled errors in the debugger. + if(dbg_pcall(lua, 0, 0, 0)){ + fprintf(stderr, "Lua Error: %s\n", lua_tostring(lua, -1)); + } + } +*/ + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct lua_State lua_State; +typedef int (*lua_CFunction)(lua_State *L); + +// This function must be called before calling dbg_pcall() to set up the debugger module. +// 'name' must be the name of the module to register the debugger as. (to use with require 'module') +// 'globalName' can either be NULL or a global variable name to assign the debugger to. (I use "dbg") +// 'readFunc' is a lua_CFunction that returns a line of input when called. Pass NULL if you want to read from stdin. +// 'writeFunc' is a lua_CFunction that takes a single string as an argument. Pass NULL if you want to write to stdout. +void dbg_setup(lua_State *lua, const char *name, const char *globalName, lua_CFunction readFunc, lua_CFunction writeFunc); + +// Same as 'dbg_setup(lua, "debugger", "dbg", NULL, NULL)' +void dbg_setup_default(lua_State *lua); + +// Drop in replacement for lua_pcall() that attaches the debugger on an error if 'msgh' is 0. +int dbg_pcall(lua_State *lua, int nargs, int nresults, int msgh); + +// Drop in replacement for luaL_dofile() +#define dbg_dofile(lua, filename) (luaL_loadfile(lua, filename) || dbg_pcall(lua, 0, LUA_MULTRET, 0)) + +#ifdef DEBUGGER_LUA_IMPLEMENTATION + +#include +#include +#include + +static const char DEBUGGER_SRC[] = {{=lua_src}}; + +int luaopen_debugger(lua_State *lua){ + if( + luaL_loadbufferx(lua, DEBUGGER_SRC, sizeof(DEBUGGER_SRC) - 1, "", NULL) || + lua_pcall(lua, 0, LUA_MULTRET, 0) + ) lua_error(lua); + + // Or you could load it from disk: + // if(luaL_dofile(lua, "debugger.lua")) lua_error(lua); + + return 1; +} + +static const char *MODULE_NAME = "DEBUGGER_LUA_MODULE"; +static const char *MSGH = "DEBUGGER_LUA_MSGH"; + +void dbg_setup(lua_State *lua, const char *name, const char *globalName, lua_CFunction readFunc, lua_CFunction writeFunc){ + // Check that the module name was not already defined. + lua_getfield(lua, LUA_REGISTRYINDEX, MODULE_NAME); + assert(lua_isnil(lua, -1) || strcmp(name, luaL_checkstring(lua, -1))); + lua_pop(lua, 1); + + // Push the module name into the registry. + lua_pushstring(lua, name); + lua_setfield(lua, LUA_REGISTRYINDEX, MODULE_NAME); + + // Preload the module + luaL_requiref(lua, name, luaopen_debugger, false); + + // Insert the msgh function into the registry. + lua_getfield(lua, -1, "msgh"); + lua_setfield(lua, LUA_REGISTRYINDEX, MSGH); + + if(readFunc){ + lua_pushcfunction(lua, readFunc); + lua_setfield(lua, -2, "read"); + } + + if(writeFunc){ + lua_pushcfunction(lua, writeFunc); + lua_setfield(lua, -2, "write"); + } + + if(globalName){ + lua_setglobal(lua, globalName); + } else { + lua_pop(lua, 1); + } +} + +void dbg_setup_default(lua_State *lua){ + dbg_setup(lua, "debugger", "dbg", NULL, NULL); +} + +int dbg_pcall(lua_State *lua, int nargs, int nresults, int msgh){ + // Call regular lua_pcall() if a message handler is provided. + if(msgh) return lua_pcall(lua, nargs, nresults, msgh); + + // Grab the msgh function out of the registry. + lua_getfield(lua, LUA_REGISTRYINDEX, MSGH); + if(lua_isnil(lua, -1)){ + luaL_error(lua, "Tried to call dbg_call() before calling dbg_setup()."); + } + + // Move the error handler just below the function. + msgh = lua_gettop(lua) - (1 + nargs); + lua_insert(lua, msgh); + + // Call the function. + int err = lua_pcall(lua, nargs, nresults, msgh); + + // Remove the debug handler. + lua_remove(lua, msgh); + + return err; +} + +#endif + +#ifdef __cplusplus +} +#endif +]==] + +local append, join, format = table.insert, table.concat, string.format + +-- return a list and a function that appends its arguments into the list +local function appender(lines) + return lines, function(...) append(lines, join{...}) end +end + +local elua = {} + +function elua.generate(template) + -- push an initial line to recieve args from the wrapping closure + local cursor, fragments, append = 1, appender{"local __elua, _ENV = ...\n"} + + while true do + -- find a code block + local i0, i1, m1, m2 = template:find("{{(=?)(.-)}}", cursor) + + if i0 == nil then + -- if no code block, append remainder and join + append("__elua", format("%q", template:sub(cursor, #template))) + return join(fragments, "; ") + elseif cursor ~= i0 then + -- if there is text to output, output it + append("__elua", format("%q", template:sub(cursor, i0 - 1))) + end + + if m1 == "=" then + -- append expression + append("__elua(", m2, ")") + else + -- append code + append(m2) + end + + cursor = i1 + 1 + end +end + +function elua.compile(template, name) + local template_code = elua.generate(template) + + -- compile the template's lua code + local chunk, err = load(template_code, name or "COMPILED TEMPLATE", "t") + if err then return nil, err end + + -- wrap chunk in closure to collect and join it's output fragments + return function(env) + local fragments, append = appender{} + chunk(append, env) + return join(fragments) + end +end + +local input_filename = arg[1] or "debugger.lua" +local output_filename = arg[2] or "debugger_lua.h" + +local lua_src = io.open(input_filename):read("a") + +-- Fix the weird escape characters +lua_src = string.format("%q", lua_src) +lua_src = string.gsub(lua_src, "\\\n", "\\n") +lua_src = string.gsub(lua_src, "\\9", "\\t") + + +local output = elua.compile(template){lua_src = lua_src} +io.open(output_filename, "w"):write(output) diff --git a/premake5.lua b/premake5.lua index 63ec85bb65..94a39db103 100644 --- a/premake5.lua +++ b/premake5.lua @@ -143,6 +143,9 @@ defines { "CURL_STATICLIB", "PREMAKE_CURL"} end + defines { "PREMAKE_DEBUGGER" } + includedirs { "contrib/debugger.lua" } + filter { "system:macosx", "options:arch=ARM or arch=ARM64" } buildoptions { "-arch arm64" } linkoptions { "-arch arm64" } diff --git a/src/host/premake.c b/src/host/premake.c index 013b8ae438..b26a307030 100644 --- a/src/host/premake.c +++ b/src/host/premake.c @@ -20,6 +20,11 @@ #include #endif +#ifdef PREMAKE_DEBUGGER +#define DEBUGGER_LUA_IMPLEMENTATION +#include +#endif + #define ERROR_MESSAGE "Error: %s\n" @@ -197,6 +202,10 @@ int premake_init(lua_State* L) luaL_register(L, "zip", zip_functions); #endif +#ifdef PREMAKE_DEBUGGER + dbg_setup_default(L); +#endif + lua_pushlightuserdata(L, &s_shimTable); lua_rawseti(L, LUA_REGISTRYINDEX, 0x5348494D); // equal to 'SHIM'