Skip to content

Commit e67798a

Browse files
committed
Set upvalues via debugger
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
1 parent 0d0f3be commit e67798a

File tree

1 file changed

+276
-1
lines changed

1 file changed

+276
-1
lines changed

src/Debug.zig

Lines changed: 276 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@
105105
//!
106106
//! ## Inspecting Call Stack
107107
//!
108-
//! Use `getInfo()`, `getArg()`, and `getLocal()` to examine the call stack at any time:
108+
//! Use `getInfo()`, `getArg()`, `getLocal()`, and `getUpvalue()` to examine the call stack at any time:
109109
//! ```zig
110110
//! const debug = lua.debug();
111111
//! const depth = debug.stackDepth();
@@ -129,6 +129,15 @@
129129
//! std.log.info("Local '{}' = {}", .{ l.name, l.value });
130130
//! }
131131
//! const name = debug.setLocal(0, 1, i32, 42); // Set first local to 42
132+
//!
133+
//! // Get and modify function upvalues
134+
//! const func = try lua.globals().get("myFunction", Lua.Function);
135+
//! defer func.?.deinit();
136+
//! const upval1 = debug.getUpvalue(func.?, 1, i32); // Get first upvalue
137+
//! if (upval1) |uv| {
138+
//! std.log.info("Upvalue '{s}' = {}", .{ uv.name, uv.value });
139+
//! }
140+
//! const upvalue_name = debug.setUpvalue(func.?, 1, i32, 99); // Set first upvalue to 99
132141
//! ```
133142
//!
134143
//! ## Single-Step Debugging
@@ -589,6 +598,110 @@ pub fn setLocal(self: Self, level: i32, n: i32, comptime T: type, value: T) ?[:0
589598
return name_ptr.?;
590599
}
591600

601+
/// Get upvalue at a specific position from a function.
602+
///
603+
/// Retrieves the value of upvalue `n` from the specified function.
604+
/// Upvalues are numbered starting from 1 (first upvalue).
605+
///
606+
/// IMPORTANT: Requires debug level 2 for Lua code compilation to get upvalue names.
607+
/// With debug level 0-1, upvalue names will be empty strings but values are still accessible.
608+
/// Use `Compiler.Opts{ .dbg_level = 2 }` when compiling Lua source for full debug information.
609+
///
610+
/// Returns a struct containing both the upvalue name and value, or null if:
611+
/// - The function reference is invalid
612+
/// - The upvalue number is out of range
613+
/// - The function is a native/C function (C functions have unnamed upvalues)
614+
///
615+
/// Example:
616+
/// ```zig
617+
/// const func = try lua.globals().get("myFunction", Lua.Function);
618+
/// defer func.?.deinit();
619+
///
620+
/// const debug = lua.debug();
621+
/// const upval1 = debug.getUpvalue(func.?, 1, i32); // Get first upvalue as i32
622+
/// if (upval1) |uv| {
623+
/// std.log.info("Upvalue '{}' = {}", .{ uv.name, uv.value });
624+
/// }
625+
/// ```
626+
///
627+
/// Parameters:
628+
/// - `func`: Function reference to inspect
629+
/// - `n`: Upvalue number (1-based indexing)
630+
/// - `T`: Type to convert the upvalue to
631+
///
632+
/// Returns: Struct with name and converted value, or null if not available
633+
pub fn getUpvalue(self: Self, func: Lua.Function, n: i32, comptime T: type) ?struct { name: [:0]const u8, value: T } {
634+
// Push function onto the stack to get its index
635+
stack.push(func.ref.lua, func.ref);
636+
defer self.state.pop(1);
637+
638+
const name_ptr = self.state.getUpvalue(-1, n);
639+
if (name_ptr == null) {
640+
return null; // Upvalue not available
641+
}
642+
643+
// Get the value from the top of the stack and pop it
644+
defer self.state.pop(1);
645+
646+
const value = stack.toValue(func.ref.lua, T, -1) orelse return null;
647+
const name = name_ptr.?;
648+
649+
return .{ .name = name, .value = value };
650+
}
651+
652+
/// Set upvalue at a specific position in a function.
653+
///
654+
/// Sets the value of upvalue `n` in the specified function.
655+
/// Upvalues are numbered starting from 1 (first upvalue).
656+
///
657+
/// IMPORTANT: Requires debug level 2 for Lua code compilation to get upvalue names.
658+
/// With debug level 0-1, upvalue names will be empty strings but values can still be set.
659+
/// Use `Compiler.Opts{ .dbg_level = 2 }` when compiling Lua source for full debug information.
660+
///
661+
/// Returns the name of the upvalue that was set, or null if:
662+
/// - The function reference is invalid
663+
/// - The upvalue number is out of range
664+
/// - The function is a native/C function (C functions have unnamed upvalues)
665+
///
666+
/// Example:
667+
/// ```zig
668+
/// const func = try lua.globals().get("myFunction", Lua.Function);
669+
/// defer func.?.deinit();
670+
///
671+
/// const debug = lua.debug();
672+
/// const name = debug.setUpvalue(func.?, 1, i32, 42); // Set first upvalue to 42
673+
/// if (name) |upvalue_name| {
674+
/// std.log.info("Set upvalue '{s}' = 42", .{upvalue_name});
675+
/// }
676+
/// ```
677+
///
678+
/// Parameters:
679+
/// - `func`: Function reference to modify
680+
/// - `n`: Upvalue number (1-based indexing)
681+
/// - `T`: Type of the value to set
682+
/// - `value`: The value to set the upvalue to
683+
///
684+
/// Returns: Name of the upvalue that was set, or null if not available
685+
pub fn setUpvalue(self: Self, func: Lua.Function, n: i32, comptime T: type, value: T) ?[:0]const u8 {
686+
// Push function onto the stack first to get its index
687+
stack.push(func.ref.lua, func.ref);
688+
689+
// Push the value onto the stack (lua_setupvalue expects value on top)
690+
stack.push(func.ref.lua, value);
691+
692+
const name_ptr = self.state.setUpvalue(-2, n); // Function is now at -2, value at -1
693+
694+
// Pop the function from stack (setUpvalue pops the value automatically)
695+
self.state.pop(1);
696+
697+
if (name_ptr == null) {
698+
// setUpvalue pops the value even on failure, so we don't need to pop it
699+
return null; // Upvalue not available
700+
}
701+
702+
return name_ptr.?;
703+
}
704+
592705
// Tests for debug functionality
593706
const expect = std.testing.expect;
594707
const expectEqual = std.testing.expectEqual;
@@ -1012,3 +1125,165 @@ test "getLocal and setLocal in debug breakpoint" {
10121125

10131126
try expect(LocalVariableTester.breakpoint_hit);
10141127
}
1128+
1129+
test "getUpvalue and setUpvalue in debug breakpoint" {
1130+
const lua = try Lua.init(&std.testing.allocator);
1131+
defer lua.deinit();
1132+
1133+
// Callback to test getUpvalue when breakpoint is hit
1134+
const UpvalueTester = struct {
1135+
var breakpoint_hit: bool = false;
1136+
1137+
pub fn debugbreak(self: *@This(), debug: *Self, ar: Info) void {
1138+
_ = self;
1139+
_ = ar;
1140+
if (!breakpoint_hit) {
1141+
breakpoint_hit = true;
1142+
1143+
// Get the current function and test its upvalues
1144+
const info = debug.getInfo(0, .{ .source = true, .name = true });
1145+
std.debug.assert(info != null);
1146+
std.debug.assert(std.mem.eql(u8, info.?.name.?, "innerFunction"));
1147+
1148+
// Get function on stack like in conformance test
1149+
var c_debug: State.Debug = undefined;
1150+
const get_result = debug.state.getInfo(0, "f", &c_debug);
1151+
std.debug.assert(get_result != 0);
1152+
// Function is now on top of stack at index -1
1153+
1154+
// Test getting first upvalue directly from stack function (like conformance test)
1155+
const upvalue_name = debug.state.getUpvalue(-1, 1);
1156+
std.debug.assert(upvalue_name != null);
1157+
std.debug.assert(std.mem.eql(u8, upvalue_name.?, "outer_var"));
1158+
// Value is now on top of stack
1159+
const upvalue_val = debug.state.toIntegerX(-1);
1160+
std.debug.assert(upvalue_val.? == 5);
1161+
debug.state.pop(1); // Pop upvalue
1162+
1163+
// Test setting the upvalue to a new value
1164+
debug.state.pushInteger(999);
1165+
const set_name = debug.state.setUpvalue(-2, 1); // -2 because we pushed a value, function is at -2
1166+
std.debug.assert(set_name != null);
1167+
std.debug.assert(std.mem.eql(u8, set_name.?, "outer_var"));
1168+
1169+
// Verify the upvalue was actually changed
1170+
const upvalue_name2 = debug.state.getUpvalue(-1, 1);
1171+
std.debug.assert(upvalue_name2 != null);
1172+
const upvalue_val2 = debug.state.toIntegerX(-1);
1173+
std.debug.assert(upvalue_val2.? == 999);
1174+
debug.state.pop(1); // Pop upvalue
1175+
1176+
// Test getting non-existent upvalue (should be null)
1177+
const upval_invalid = debug.state.getUpvalue(-1, 10);
1178+
std.debug.assert(upval_invalid == null);
1179+
1180+
debug.state.pop(1); // Pop function from stack
1181+
}
1182+
}
1183+
};
1184+
1185+
var tester = UpvalueTester{};
1186+
lua.setCallbacks(&tester);
1187+
1188+
// Create a function with upvalues like in the conformance test
1189+
const code =
1190+
\\local outer_var = 5
1191+
\\function innerFunction()
1192+
\\ return outer_var * 2 -- Breakpoint will be set on this line
1193+
\\end
1194+
;
1195+
1196+
// Compile with debug level 2 for upvalue names and breakpoint support
1197+
const Compiler = @import("Compiler.zig");
1198+
var options = Compiler.Opts{};
1199+
options.dbg_level = 2;
1200+
options.opt_level = 0; // Don't optimize away upvalues
1201+
1202+
const result = try Compiler.compile(code, options);
1203+
defer result.deinit();
1204+
switch (result) {
1205+
.ok => |bytecode| {
1206+
_ = try lua.exec(bytecode, void);
1207+
},
1208+
.err => |message| {
1209+
std.debug.print("Compile error: {s}\n", .{message});
1210+
return error.CompileError;
1211+
},
1212+
}
1213+
1214+
const func = try lua.globals().get("innerFunction", Lua.Function);
1215+
defer func.?.deinit();
1216+
1217+
// Try line 3 (function start is line 2, line 3 is return)
1218+
const actual_line = try func.?.setBreakpoint(3, true);
1219+
try expectEqual(actual_line, 3);
1220+
1221+
const call_result = try func.?.call(.{}, i32);
1222+
try expectEqual(call_result.ok.?, 1998); // 999 * 2 (modified upvalue)
1223+
1224+
try expect(UpvalueTester.breakpoint_hit);
1225+
}
1226+
1227+
test "high-level getUpvalue and setUpvalue API" {
1228+
const lua = try Lua.init(&std.testing.allocator);
1229+
defer lua.deinit();
1230+
1231+
// Create a function with upvalues
1232+
const code =
1233+
\\local shared_value = 42
1234+
\\function closure()
1235+
\\ return shared_value
1236+
\\end
1237+
;
1238+
1239+
// Compile with debug level 2 for upvalue names
1240+
const Compiler = @import("Compiler.zig");
1241+
var options = Compiler.Opts{};
1242+
options.dbg_level = 2;
1243+
options.opt_level = 0; // Don't optimize away upvalues
1244+
1245+
const result = try Compiler.compile(code, options);
1246+
defer result.deinit();
1247+
switch (result) {
1248+
.ok => |bytecode| {
1249+
_ = try lua.exec(bytecode, void);
1250+
},
1251+
.err => |message| {
1252+
std.debug.print("Compile error: {s}\n", .{message});
1253+
return error.CompileError;
1254+
},
1255+
}
1256+
1257+
const func = try lua.globals().get("closure", Lua.Function);
1258+
defer func.?.deinit();
1259+
1260+
const debug = lua.debug();
1261+
1262+
// Test getting upvalue using high-level API
1263+
const upval1 = debug.getUpvalue(func.?, 1, i32);
1264+
try expect(upval1 != null);
1265+
try expectEqual(upval1.?.value, 42);
1266+
try expect(std.mem.eql(u8, upval1.?.name, "shared_value"));
1267+
1268+
// Test setting upvalue using high-level API
1269+
const set_name = debug.setUpvalue(func.?, 1, i32, 100);
1270+
try expect(set_name != null);
1271+
try expect(std.mem.eql(u8, set_name.?, "shared_value"));
1272+
1273+
// Verify the upvalue was changed
1274+
const upval2 = debug.getUpvalue(func.?, 1, i32);
1275+
try expect(upval2 != null);
1276+
try expectEqual(upval2.?.value, 100);
1277+
1278+
// Test the function returns the modified upvalue
1279+
const call_result = try func.?.call(.{}, i32);
1280+
try expectEqual(call_result.ok.?, 100);
1281+
1282+
// Test getting non-existent upvalue
1283+
const upval_invalid = debug.getUpvalue(func.?, 10, i32);
1284+
try expect(upval_invalid == null);
1285+
1286+
// Test setting non-existent upvalue
1287+
const set_invalid = debug.setUpvalue(func.?, 10, i32, 42);
1288+
try expect(set_invalid == null);
1289+
}

0 commit comments

Comments
 (0)