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.