Skip to content

Commit dd4f2da

Browse files
committed
Implement debugBreak
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
1 parent d61f1b1 commit dd4f2da

File tree

5 files changed

+262
-45
lines changed

5 files changed

+262
-45
lines changed

CLAUDE.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,20 @@ fn transform(upv: Upvalues(struct { f32, f32 }), x: f32) f32 {
126126
try table.setClosure("transform", .{ 2.0, 10.0 }, transform);
127127
```
128128

129+
## Luau Submodule
130+
131+
This repository includes the full Luau source code as a Git submodule located at `/Users/mpavlenko/Github/luaz/luau`. When investigating Luau implementation details, behavior, or test patterns, use this submodule instead of searching external repositories. The submodule contains:
132+
133+
- VM source code in `luau/VM/src/`
134+
- Test files in `luau/tests/` (C++ unit tests) and `luau/tests/conformance/` (Luau test scripts)
135+
- Debug API implementation files like `ldebug.cpp`, `ldo.cpp`, and the main header `lua.h`
136+
- Conformance tests that demonstrate proper usage patterns for debug functionality
137+
138+
Key test files for understanding debug features:
139+
- `luau/tests/Conformance.test.cpp` - Contains C++ test code showing how to use `lua_break`, `debuginterrupt`, and breakpoint functionality
140+
- `luau/tests/conformance/interrupt.luau` - Luau script for testing interrupt functionality
141+
- `luau/tests/conformance/debugger.luau` - Luau script demonstrating breakpoint usage
142+
129143
## Development Patterns
130144

131145
### Versioning and Compatibility
@@ -147,13 +161,20 @@ corresponding unit tests. Tests use `&std.testing.allocator` for memory leak det
147161

148162
Keep unit tests minimal and focused. Tests must demonstrate that functionality works.
149163
Write clear, concise tests that verify the feature without unnecessary complexity.
164+
Keep unit tests short and understandable.
150165

151166
Important testing guidelines:
152167
- `tests.zig` should include unit tests that test only public APIs
153168
- Avoid using functions from `stack.zig` and `State.zig` in `tests.zig`
169+
- Never use `stack.*` functions when testing public APIs - use only the high-level API methods
154170
- Focus on testing the high-level API provided by `lua.zig`
155171
- Don't create excessive and too verbose unit tests
156172
- Each test must be minimal and aim to test a specific function
173+
- Keep tests short, focused, and easy to understand
174+
- Write meaningful tests that actually test the APIs and their intended behavior
175+
- Tests must verify that specific API methods work correctly, not just that code runs without crashing
176+
- Use specific assertions that validate expected outcomes rather than always-true conditions
177+
- Each test should exercise actual API functionality and verify the correct behavior of the methods being tested
157178

158179
### Documentation
159180
Keep documentation for public interfaces current but reasonably sized. The codebase uses Zig's built-in doc comments

src/State.zig

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -761,8 +761,10 @@ pub inline fn yield(self: Self, nresults: u32) Status {
761761
}
762762

763763
/// Break execution
764-
pub inline fn break_(self: Self) Status {
765-
return @enumFromInt(c.lua_break(self.lua));
764+
pub inline fn break_(self: Self) void {
765+
_ = c.lua_break(self.lua);
766+
// lua_break always returns -1, which is not meaningful
767+
// The function sets L->status = LUA_BREAK internally
766768
}
767769

768770
/// Resume coroutine

src/debug.zig

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,89 @@
1+
//! Debug functionality for Luau scripts.
2+
//!
3+
//! This module provides debugging support for Luau scripts, including breakpoints,
4+
//! single-stepping, and execution interruption. The debug system uses a callback-based
5+
//! approach where the VM notifies your application when debug events occur.
6+
//!
7+
//! ## Basic Usage
8+
//!
9+
//! 1. Set up debug callbacks using `lua.setCallbacks()`:
10+
//! ```zig
11+
//! const DebugCallbacks = struct {
12+
//! pub fn debugbreak(self: *@This(), lua: *Lua, ar: Debug) void {
13+
//! std.log.info("Hit breakpoint at line {}", .{ar.current_line});
14+
//! // Call debugBreak() to actually interrupt execution
15+
//! lua.debugBreak();
16+
//! }
17+
//! };
18+
//!
19+
//! var callbacks = DebugCallbacks{};
20+
//! lua.setCallbacks(&callbacks);
21+
//! ```
22+
//!
23+
//! 2. Set breakpoints on functions using `function.setBreakpoint(line)`:
24+
//! ```zig
25+
//! // Lua code to debug
26+
//! const code =
27+
//! \\function myFunction()
28+
//! \\ local x = 10
29+
//! \\ return x + 5 -- This is line 3
30+
//! \\end
31+
//! ;
32+
//!
33+
//! _ = try lua.eval(code, .{}, void);
34+
//! const func = try lua.globals().get("myFunction", Function);
35+
//! defer func.deinit();
36+
//!
37+
//! // Set breakpoint on line 3 of the function
38+
//! const actual_line = try func.setBreakpoint(3, true);
39+
//! ```
40+
//!
41+
//! 3. Handle interrupted execution:
42+
//! ```zig
43+
//! const result = func.call(.{}, i32) catch |err| switch (err) {
44+
//! error.Break => {
45+
//! // Execution was interrupted at breakpoint
46+
//! // Can examine state, variables, etc.
47+
//!
48+
//! // Resume execution by calling the function again
49+
//! return func.call(.{}, i32);
50+
//! },
51+
//! else => return err,
52+
//! };
53+
//! ```
54+
//!
55+
//! ## Debug Flow
56+
//!
57+
//! The debugging process follows this flow:
58+
//! 1. Breakpoint hit → VM calls your `debugbreak` callback
59+
//! 2. In callback → Call `lua.debugBreak()` to interrupt execution
60+
//! 3. VM interrupts → Sets internal status to LUA_BREAK
61+
//! 4. Function returns → `error.Break` to your application code
62+
//! 5. Resume execution → Call the function again to continue from where it left off
63+
//!
64+
//! ## Key Concepts
65+
//!
66+
//! - Breakpoints are notifications: Setting breakpoints with `function.setBreakpoint()` only triggers
67+
//! the callback - it doesn't automatically stop execution.
68+
//! - debugBreak() stops execution: You must call `lua.debugBreak()` within your
69+
//! callback to actually interrupt and return control to your application.
70+
//! - Resumption: After `error.Break`, call the same function again to resume execution.
71+
//! - Debug info limitations: Only `current_line` and `userdata` fields are populated
72+
//! in debug callbacks. Other fields contain garbage values.
73+
//!
74+
//! ## Advanced Features
75+
//!
76+
//! - Single stepping: Use `lua.setSingleStep(true)` and the `debugstep` callback
77+
//! - Thread interruption: Use `debuginterrupt` callback for coroutine debugging
78+
//! - Function breakpoints: Use `function.setBreakpoint(line)` to set breakpoints on specific functions
79+
//! - Conditional breakpoints: Only call `debugBreak()` when certain conditions are met
80+
//!
81+
//! ## Debug Information
82+
//!
83+
//! The `Debug` struct contains information about the current execution context,
84+
//! but note that most fields are only populated during `lua_getinfo()` calls,
85+
//! not during debug hook callbacks.
86+
187
const std = @import("std");
288
const State = @import("State.zig");
389

@@ -21,6 +107,8 @@ pub const Debug = struct {
21107
param_count: u8,
22108
/// Whether function accepts variable arguments
23109
is_vararg: bool,
110+
/// Optional user data (for debuginterrupt, contains the interrupted thread state)
111+
userdata: ?*anyopaque,
24112

25113
/// Creates a Debug struct from a C lua_Debug pointer
26114
///
@@ -37,15 +125,27 @@ pub const Debug = struct {
37125
/// function on the stack which is more complex.
38126
pub fn fromC(c_debug: *State.Debug) Debug {
39127
return Debug{
40-
.name = if (c_debug.name) |n| std.mem.span(n) else null,
41-
.what = if (c_debug.what) |w| std.mem.span(w) else "unknown",
42-
.source = if (c_debug.source) |s| std.mem.span(s) else "unknown",
43-
.short_src = if (c_debug.short_src) |s| std.mem.span(s) else "unknown",
128+
.name = null,
129+
.what = "",
130+
.source = "",
131+
.short_src = "",
44132
.line_defined = c_debug.linedefined,
45133
.current_line = c_debug.currentline,
46134
.upvalue_count = c_debug.nupvals,
47135
.param_count = c_debug.nparams,
48136
.is_vararg = c_debug.isvararg != 0,
137+
.userdata = c_debug.userdata,
49138
};
50139
}
140+
141+
/// Gets the interrupted thread from debuginterrupt callback userdata.
142+
/// Returns null if userdata is null or not a valid thread state.
143+
/// Only valid when called from debuginterrupt callback.
144+
pub fn getInterruptedThread(self: Debug) ?State {
145+
if (self.userdata) |data| {
146+
const lua_state: State.LuaState = @ptrCast(@alignCast(data));
147+
return State{ .lua = lua_state };
148+
}
149+
return null;
150+
}
51151
};

src/lua.zig

Lines changed: 73 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ pub const Lua = struct {
7373
Runtime,
7474
/// Breakpoint could not be set at the specified line.
7575
InvalidBreakpoint,
76+
/// Debug break occurred during execution.
77+
Break,
7678
};
7779

7880
/// Assert handler function type for Luau VM assertions.
@@ -213,10 +215,13 @@ pub const Lua = struct {
213215
/// - `panic(state: *State, errcode: i32) void` - Called on unprotected errors (if longjmp is used)
214216
/// - `userthread(parent: ?*State, thread: *State) void` - Called when thread is created/destroyed
215217
/// - `useratom(s: []const u8) i16` - Called when string is created; returns atom ID
216-
/// - `debugbreak(state: *State, ar: Debug) void` - Called on BREAK instruction
217-
/// - `debugstep(state: *State, ar: Debug) void` - Called after each instruction in single step
218-
/// - `debuginterrupt(state: *State, ar: Debug) void` - Called on thread execution interrupt
219-
/// - `debugprotectederror(state: *State) void` - Called when protected call results in error
218+
/// - `debugbreak(lua: *Lua, ar: Debug) void` - Called when breakpoint is hit. Note that breakpoints
219+
/// set with `breakpoint(line)` in Lua code only trigger this callback - they don't automatically
220+
/// interrupt execution. Call `lua.debugBreak()` within this callback to actually interrupt
221+
/// execution and return `error.Break` to the caller.
222+
/// - `debugstep(lua: *Lua, ar: Debug) void` - Called after each instruction in single step
223+
/// - `debuginterrupt(lua: *Lua, ar: Debug) void` - Called on thread execution interrupt
224+
/// - `debugprotectederror(lua: *Lua) void` - Called when protected call results in error
220225
/// - `onallocate(state: *State, osize: usize, nsize: usize) void` - Called when a memory operation occurs
221226
/// (allocation when osize=0, deallocation when nsize=0, reallocation otherwise).
222227
/// Note: This callback is only triggered for Luau's internal allocations, not for all memory operations
@@ -348,15 +353,15 @@ pub const Lua = struct {
348353
if (@hasDecl(CallbackType, "debugbreak")) {
349354
cb.debugbreak = struct {
350355
fn wrapper(L: ?State.LuaState, ar: ?*State.Debug) callconv(.C) void {
351-
var state = State{ .lua = L.? };
356+
var lua = Lua.fromState(L.?);
352357
const debug_info = debug.Debug.fromC(ar.?);
353358

354359
if (comptime is_instance) {
355-
const callbacks_struct = state.callbacks();
360+
const callbacks_struct = lua.state.callbacks();
356361
const instance: *CallbackType = @ptrCast(@alignCast(callbacks_struct.userdata.?));
357-
instance.debugbreak(&state, debug_info);
362+
instance.debugbreak(&lua, debug_info);
358363
} else {
359-
CallbackType.debugbreak(&state, debug_info);
364+
CallbackType.debugbreak(&lua, debug_info);
360365
}
361366
}
362367
}.wrapper;
@@ -365,15 +370,15 @@ pub const Lua = struct {
365370
if (@hasDecl(CallbackType, "debugstep")) {
366371
cb.debugstep = struct {
367372
fn wrapper(L: ?State.LuaState, ar: ?*State.Debug) callconv(.C) void {
368-
var state = State{ .lua = L.? };
373+
var lua = Lua.fromState(L.?);
369374
const debug_info = debug.Debug.fromC(ar.?);
370375

371376
if (comptime is_instance) {
372-
const callbacks_struct = state.callbacks();
377+
const callbacks_struct = lua.state.callbacks();
373378
const instance: *CallbackType = @ptrCast(@alignCast(callbacks_struct.userdata.?));
374-
instance.debugstep(&state, debug_info);
379+
instance.debugstep(&lua, debug_info);
375380
} else {
376-
CallbackType.debugstep(&state, debug_info);
381+
CallbackType.debugstep(&lua, debug_info);
377382
}
378383
}
379384
}.wrapper;
@@ -382,15 +387,15 @@ pub const Lua = struct {
382387
if (@hasDecl(CallbackType, "debuginterrupt")) {
383388
cb.debuginterrupt = struct {
384389
fn wrapper(L: ?State.LuaState, ar: ?*State.Debug) callconv(.C) void {
385-
var state = State{ .lua = L.? };
390+
var lua = Lua.fromState(L.?);
386391
const debug_info = debug.Debug.fromC(ar.?);
387392

388393
if (comptime is_instance) {
389-
const callbacks_struct = state.callbacks();
394+
const callbacks_struct = lua.state.callbacks();
390395
const instance: *CallbackType = @ptrCast(@alignCast(callbacks_struct.userdata.?));
391-
instance.debuginterrupt(&state, debug_info);
396+
instance.debuginterrupt(&lua, debug_info);
392397
} else {
393-
CallbackType.debuginterrupt(&state, debug_info);
398+
CallbackType.debuginterrupt(&lua, debug_info);
394399
}
395400
}
396401
}.wrapper;
@@ -399,14 +404,14 @@ pub const Lua = struct {
399404
if (@hasDecl(CallbackType, "debugprotectederror")) {
400405
cb.debugprotectederror = struct {
401406
fn wrapper(L: ?State.LuaState) callconv(.C) void {
402-
var state = State{ .lua = L.? };
407+
var lua = Lua.fromState(L.?);
403408

404409
if (comptime is_instance) {
405-
const callbacks_struct = state.callbacks();
410+
const callbacks_struct = lua.state.callbacks();
406411
const instance: *CallbackType = @ptrCast(@alignCast(callbacks_struct.userdata.?));
407-
instance.debugprotectederror(&state);
412+
instance.debugprotectederror(&lua);
408413
} else {
409-
CallbackType.debugprotectederror(&state);
414+
CallbackType.debugprotectederror(&lua);
410415
}
411416
}
412417
}.wrapper;
@@ -415,8 +420,8 @@ pub const Lua = struct {
415420
if (@hasDecl(CallbackType, "onallocate")) {
416421
cb.onallocate = struct {
417422
fn wrapper(L: ?State.LuaState, osize: usize, nsize: usize) callconv(.C) void {
418-
if (L) |lua| {
419-
var state = State{ .lua = lua };
423+
if (L) |lua_state| {
424+
var state = State{ .lua = lua_state };
420425

421426
if (comptime is_instance) {
422427
const callbacks_struct = state.callbacks();
@@ -471,6 +476,50 @@ pub const Lua = struct {
471476
self.state.singleStep(enabled);
472477
}
473478

479+
/// Interrupt thread execution during debug callbacks.
480+
///
481+
/// This method should be called from within debug callbacks (debugbreak, debugstep, debuginterrupt)
482+
/// to interrupt the current execution and return control to the caller. When called, it sets the
483+
/// VM's status to LUA_BREAK, causing the current function to return with `error.Break`.
484+
///
485+
/// Note: Breakpoints set with `breakpoint(line)` in Lua code only trigger debug callbacks - they
486+
/// don't automatically interrupt execution. You must call `debugBreak()` within your callback
487+
/// to actually interrupt and return control to your application.
488+
///
489+
/// Flow:
490+
/// 1. Breakpoint hits → VM calls your debugbreak callback
491+
/// 2. In callback → call `debugBreak()` to interrupt execution
492+
/// 3. VM sets status → L.status = LUA_BREAK
493+
/// 4. Function returns → `error.Break` to caller
494+
/// 5. Can resume → call function again to continue from where it left off
495+
///
496+
/// Example:
497+
/// ```zig
498+
/// const DebugCallbacks = struct {
499+
/// pub fn debugbreak(self: *@This(), lua: *Lua, ar: Debug) void {
500+
/// std.log.info("Hit breakpoint at line {}", .{ar.current_line});
501+
/// // This actually interrupts execution and returns error.Break
502+
/// lua.debugBreak();
503+
/// }
504+
/// };
505+
///
506+
/// var callbacks = DebugCallbacks{};
507+
/// lua.setCallbacks(&callbacks);
508+
///
509+
/// // When breakpoint hits, function returns error.Break instead of continuing
510+
/// const result = func.call(.{}, i32) catch |err| switch (err) {
511+
/// error.Break => {
512+
/// // Execution was interrupted, can examine state or resume later
513+
/// return func.call(.{}, i32); // Resume execution
514+
/// },
515+
/// else => return err,
516+
/// };
517+
/// ```
518+
///
519+
pub fn debugBreak(self: Self) void {
520+
self.state.break_();
521+
}
522+
474523
/// A reference to a Lua value.
475524
///
476525
/// Holds a reference ID that can be used to retrieve the value later.
@@ -833,6 +882,7 @@ pub const Lua = struct {
833882
const result = self.ref.lua.call(args, R, false);
834883
return switch (result.status) {
835884
.ok, .yield => result.result.?,
885+
.break_debug => error.Break,
836886
.errmem => error.OutOfMemory,
837887
else => error.Runtime,
838888
};
@@ -1662,6 +1712,7 @@ pub const Lua = struct {
16621712

16631713
return switch (result.status) {
16641714
.ok, .yield => result.result.?,
1715+
.break_debug => error.Break,
16651716
.errmem => error.OutOfMemory,
16661717
else => error.Runtime,
16671718
};

0 commit comments

Comments
 (0)