LuaProbe — small source-level de bugger for Lua 5.1 / LuaJIT (two files, no C deps)
António Cardoso <[email protected]> Wed, 29 Apr 2026 02:05:20 -0700 (PDT)
| Newsgroups | gmane.comp.lang.lua.general |
|---|---|
| Message-ID | <[email protected]> |
Hi. I've been debugging a fairly large Lua 5.1 codebase at work and got
tired of sprinkling print() everywhere, so I ended up writing a proper
debugger and just open-sourced it.
https://github.com/PlugwiseBV/LuaProbe
(Heads up: most of the code was written with Claude. I drove the design,
did the level-discipline debugging, and tested it against our actual
codebase — but credit where it's due, the AI did a lot of the typing.)
The whole thing is two files you drop into a project. No luasocket, no
luaposix, no C extensions. The child-side stub is plain Lua 5.1 and
attaches via LUA_INIT. The controller is LuaJIT and talks to the child over
a pair of FIFOs. There's a small CLI (bin/luaprobe) that wraps the library
so you can use it like gdb:
bin/luaprobe -b demo.lua:7 demo.lua
bin/luaprobe -b 'demo.lua:7 if i > 1' demo.lua
It does most of the obvious things: stop and log-only breakpoints,
snap-forward (so you don't have to pick a line that the compiler actually
emitted opcodes for), step / next / finish / continue, full stack walks
with locals and upvalues, deep table dumps with cycle handling, breakpoints
that fire inside coroutines, and live add/remove of breakpoints during a
pause.
A couple of things I find genuinely useful:
Conditional breakpoints — foo.lua:42 if user.id == target_id. The condition
is a Lua expression evaluated against the frame's locals/upvalues with _G
as fallback. Typos in the condition just silently never fire
Eval during pause — at the prompt, e some_expression runs in the paused
frame's scope. Locals, upvalues, and globals are all visible. Useful when
you want to poke at a value without setting a new breakpoint.
Entry-time snapshots — alongside each frame's current locals you also get
the values they had on function entry. Helps when you're trying to figure
out "wait, was this nil when we got called, or did we clobber it ourselves?"
A few honest caveats: it's Linux-only (uses the O_RDWR | O_NONBLOCK FIFO
trick), the line hook adds 2-4x slowdown during a debug session,
breakpoints only snap forward, eval is read-only on locals (writes don't
persist back to the frame), and coroutines created by C code via
lua_newthread are invisible.
The single most error-prone part of writing a Lua debugger turned out to be
stack-level discipline — debug.sethook only guarantees getinfo(2) is user
code if the hook is the directly-registered function, so any wrapper
between sethook and the body silently shifts every level by one and you
spend a day wondering why your locals are full of stub internals. There's a
section in DEBUGGER.md about that, and the comments in luaprobe_stub.lua
are pretty paranoid about it.
MIT-licensed. Happy to answer questions or take suggestions.
--
You received this message because you are subscribed to the Google Groups "lua-l" group.
To unsubscribe from this group and stop receiving emails from it, send an email to [email protected].
To view this discussion visit https://groups.google.com/d/msgid/lua-l/891052a2-25df-411e-a494-110e34dc69d9n%40googlegroups.com.