LuaProbe — small source-level debugger for Lua 5.1 / LuaJIT (two files, no C deps)

57 views
Skip to first unread message

António Cardoso

unread,
Apr 29, 2026, 6:25:40 AM (4 days ago) Apr 29
to lua-l
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.                          


(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.      

Martin Eden

unread,
Apr 29, 2026, 8:39:09 AM (4 days ago) Apr 29
to lu...@googlegroups.com
On 2026-04-29 11:05, António Cardoso wrote:
> 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
>
> [...]
>
> MIT-licensed. Happy to answer questions or take suggestions.
>
Nice repo,

I like it's short and complete code. And also kudos for documenting
input/output table structures.

I have rare luxury of using my own codebase. Which has zero print's().
When I need to debug something I just insert print() where I want,
then run program and read results. So REPL is no use for me.

Can debugger's discovery methods can be exposed to program?
So when debugging I can insert something like "LuaProbe:PrintLocals()".

-- Martin

António Cardoso

unread,
Apr 29, 2026, 8:56:04 AM (4 days ago) Apr 29
to lua-l

Hi Martin,

Two things regarding your question.

First, keep in mind that LuaProbe is responsible for starting your Lua program. Unlike some other debuggers that can be attached midway through execution, LuaProbe must be enabled from the start. This means your software must either run in debug mode or not at all. That’s important because debugging can slow execution by a factor of two to four. If your application has time-critical sections, this may cause issues.

Second, and more directly related to your question: you don’t need to insert anything into your code for this. LuaProbe supports both traditional breakpoints and log-only breakpoints. A log-only breakpoint will not halt execution; it simply updates the output when reached. You can also attach conditions to these breakpoints.

For example:
foo.lua:42! if user.id == target_id

The ! makes it a log-only breakpoint. Without it, the breakpoint behaves like a normal stopping breakpoint.

Best Regards
- Antonio Cardoso 

Martin Eden

unread,
Apr 29, 2026, 9:24:12 AM (4 days ago) Apr 29
to lu...@googlegroups.com
On 2026-04-29 14:56, António Cardoso wrote:
> Hi Martin,
>
> Two things regarding your question.
>
> First, keep in mind that LuaProbe is responsible for starting your Lua
> program. Unlike some other debuggers that can be attached midway through
> execution, LuaProbe must be enabled from the start. This means your
> software must either run in debug mode or not at all. That’s important
> because debugging can slow execution by a factor of two to four. If your
> application has time-critical sections, this may cause issues.
>
> Second, and more directly related to your question: you don’t need to
> insert anything into your code for this. LuaProbe supports both traditional
> breakpoints and log-only breakpoints. A log-only breakpoint will not halt
> execution; it simply updates the output when reached. You can also attach
> conditions to these breakpoints.
>
> For example:
> foo.lua:42! if user.id == target_id
>
> The ! makes it a log-only breakpoint. Without it, the breakpoint behaves
> like a normal stopping breakpoint.
>
> Best Regards
> - Antonio Cardoso

Hi Antonio,

Its okay, that debugger will load program, not vice versa.

What will be useful for my case is non-interactive debugger.

So I won't spend calories on smashing keys in debugger's shell.
Instead in code I'll add something like

  local some_very_tricky_condition
  -- [...]
  if some_very_tricky_condition then
    LuaProbe:PrintState(
      {
        Locals = true,
        Stacktrace = false,
        Upvalues = false,
      }
    )
  end

-- Martin

António Cardoso

unread,
Apr 29, 2026, 10:17:07 AM (4 days ago) Apr 29
to lua-l
Hi Martin,

Added now a way to do what you want (more or less). This repo will never allow you to add something like LuaProbe:PrintState because one of the reasons for its creation was to avoid injecting code in the codebase.
Instead you can run like this 2 examples, where by setting up as log-only breakpoints by adding the ! simbol to the expression. 

bin/luaprobe -b 'examples/demo.lua:7![locals,stack] if i == 2' examples/demo.lua
luaprobe: launching: lua5.1 examples/demo.lua
luaprobe:   breakpoint: examples/demo.lua:7![locals,stack] if i == 2
luaprobe: waiting for events (Ctrl-C to quit)
hello, world (1)
hello, world (2)
hello, world (3)
luaprobe: child attached

[LOG] examples/demo.lua:7  [main]
 #1  greet                    examples/demo.lua:7
 #2  <main>                   examples/demo.lua:11
 #3  [C]                      =[C]:-1
 locals:
   name = "world"
   times = 3
   message = "hello, world"
   i = 2


and here it is without any condition and just removing the stack

bin/luaprobe -b 'examples/demo.lua:7![-stack]' examples/demo.lua      
luaprobe: launching: lua5.1 examples/demo.lua
luaprobe:   breakpoint: examples/demo.lua:7![-stack]
luaprobe: waiting for events (Ctrl-C to quit)
hello, world (1)
hello, world (2)
hello, world (3)
luaprobe: child attached

[LOG] examples/demo.lua:7  [main]
 #1  greet                    examples/demo.lua:7
 locals:
   name = "world"
   times = 3
   message = "hello, world"
   i = 1

[LOG] examples/demo.lua:7  [main]
 #1  greet                    examples/demo.lua:7
 locals:
   name = "world"
   times = 3
   message = "hello, world"
   i = 2

[LOG] examples/demo.lua:7  [main]
 #1  greet                    examples/demo.lua:7
 locals:
   name = "world"
   times = 3
   message = "hello, world"
   i = 3

You can of course then setup your own GUI or custom interface for whatever editor you choose, we will be also publishing in a near future a proper TUI using lua-curses and an emacs extension. bin/luaprobe is already a small CLI for it but a very mininal one and it might get in the way

-Antonio Cardoso

Martin Eden

unread,
Apr 29, 2026, 10:48:49 AM (4 days ago) Apr 29
to lu...@googlegroups.com
Thanks for changes!

-- Martin

On 2026-04-29 16:17, António Cardoso wrote:
> Hi Martin,
>
Reply all
Reply to author
Forward
0 new messages