Skip to content

feat(luau): expose native breakpoint and step debug API#705

Open
w4nderlust wants to merge 1 commit into
mlua-rs:mainfrom
w4nderlust:feat/luau-debug-api
Open

feat(luau): expose native breakpoint and step debug API#705
w4nderlust wants to merge 1 commit into
mlua-rs:mainfrom
w4nderlust:feat/luau-debug-api

Conversation

@w4nderlust
Copy link
Copy Markdown

On the Luau backend there is currently no way to do line-precise debugging from mlua. Lua::set_hook is #[cfg(not(feature = "luau"))], set_interrupt only fires at coarse safepoints (calls, loop back-edges, returns) so it cannot stop on an arbitrary line, and the Luau sandbox strips debug.getlocal/getfenv so even when you do pause you cannot read locals.

Luau's C API already has everything needed for this, mlua just had not wrapped it. This PR adds safe wrappers following the existing set_interrupt pattern. No mlua-sys changes, the ffi was already declared.

New surface, all behind the luau feature:

  • Lua::set_debug_break / set_debug_step (plus removers) and set_single_step. The callbacks receive a &Debug for the paused frame and can return VmState::Yield to suspend the running coroutine, same semantics as set_interrupt.
  • Function::set_breakpoint(line, enabled) -> Option<u32>, wrapping lua_breakpoint. Returns the line Luau actually placed it on (it snaps the breakpoint to the next executable line).
  • Debug::get_local / set_local / locals, wrapping lua_getlocal/lua_setlocal. Luau keeps locals reachable here even though the sandbox removes the debug library equivalents.

Two things worth knowing, both called out in the docs/comments:

  • Luau re-evaluates a breakpoint when the coroutine is resumed, so the break callback fires again on the same line. The expected pattern is to yield on the first hit and return VmState::Continue on resume to step past it. The tests exercise this.
  • The break/step callbacks cannot go through callback_error_ext. It reserves its WrappedFailure userdata at the base of the running frame (lua_insert(state, 1)), which shifts the paused function's registers up by one and makes lua_getlocal read the wrong slots. So there is a small dedicated debug_callback trampoline that reserves at the top of the stack instead, keeping the same error/panic/yield handling.

Tests in tests/luau.rs cover breakpoint pause with yield/resume in a coroutine, a breakpoint on a multi-line call expression, single-step over consecutive lines and disabling it, reading and writing locals at a breakpoint, and error propagation from a break callback. The chunks are compiled with optimization 0 / debug 2 so line and local mapping stays stable.

Wrap Luau's debug C API so embedders can build line-precise debuggers:
lua_breakpoint, lua_singlestep, lua_getlocal/lua_setlocal, and the
debugbreak/debugstep callbacks. The callbacks mirror set_interrupt and can
return VmState::Yield to suspend the running coroutine.

New surface (all gated on the luau feature):
- Lua::set_debug_break / set_debug_step / set_single_step and their removers
- Function::set_breakpoint
- Debug::get_local / set_local / locals

The debug callbacks need their own trampoline instead of callback_error_ext:
the latter reserves its WrappedFailure at the running frame's base, which
shifts the paused function's registers and makes lua_getlocal read the wrong
slots. debug_callback reserves at the top of the stack instead.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant