From 442888c6c306cfff0708c3d4bbbeda685cec2e75 Mon Sep 17 00:00:00 2001 From: t Date: Tue, 2 Jun 2026 19:41:56 -0600 Subject: lua /commands --- src/lua_bridge.zig | 162 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 159 insertions(+), 3 deletions(-) (limited to 'src/lua_bridge.zig') diff --git a/src/lua_bridge.zig b/src/lua_bridge.zig index eca5661..cb57ade 100644 --- a/src/lua_bridge.zig +++ b/src/lua_bridge.zig @@ -77,6 +77,12 @@ pub const BridgeError = error{ /// needs to harvest entries. pub var registrations_key: u8 = 0; +/// The key under which we stash the *command* registrations table in +/// `LUA_REGISTRYINDEX`. Distinct from `registrations_key` so tool and +/// slash-command registrations never collide. Holds an array of records +/// shaped `{ name=, description=, handler= }`. +pub var command_registrations_key: u8 = 0; + /// A single declared tool, as harvested from a script's top-level call to /// `panto.register_tool`. All slices reference Lua-owned strings on the /// state's stack/registry; copy them before closing the state. @@ -87,6 +93,14 @@ pub const Registration = struct { schema_json: []const u8, }; +/// A single declared slash command, harvested from a script's top-level +/// call to `panto.register_command`. Slices reference Lua-owned strings; +/// copy them before closing the state. +pub const CommandRegistration = struct { + name: []const u8, + description: []const u8, +}; + // --------------------------------------------------------------------------- // Public bridge API // --------------------------------------------------------------------------- @@ -102,10 +116,17 @@ pub fn install(L: *c.lua_State) void { // Stash under our registry key. c.lua_rawsetp(L, LUA_REGISTRYINDEX, ®istrations_key); - // Build the `panto` global table with `register_tool`. - c.lua_createtable(L, 0, 1); + // Create the command registrations table (parallel to the tool one). + c.lua_createtable(L, 0, 0); + c.lua_rawsetp(L, LUA_REGISTRYINDEX, &command_registrations_key); + + // Build the `panto` global table with `register_tool` and + // `register_command`. + c.lua_createtable(L, 0, 2); c.lua_pushcclosure(L, registerToolThunk, 0); c.lua_setfield(L, -2, "register_tool"); + c.lua_pushcclosure(L, registerCommandThunk, 0); + c.lua_setfield(L, -2, "register_command"); c.lua_setglobal(L, "panto"); } @@ -116,6 +137,8 @@ pub fn install(L: *c.lua_State) void { pub fn resetRegistrations(L: *c.lua_State) void { c.lua_createtable(L, 0, 0); c.lua_rawsetp(L, LUA_REGISTRYINDEX, ®istrations_key); + c.lua_createtable(L, 0, 0); + c.lua_rawsetp(L, LUA_REGISTRYINDEX, &command_registrations_key); } /// Load and execute a Lua source file in the given state. The file's @@ -164,6 +187,33 @@ pub fn harvestRegistrations( return out; } +/// Walk the *command* registrations table and copy each entry's name and +/// description into arena-owned bytes. The handler field is ignored; +/// callers that need to invoke handlers `luaL_ref` them separately. +pub fn harvestCommandRegistrations( + L: *c.lua_State, + arena: Allocator, +) BridgeError![]CommandRegistration { + _ = c.lua_rawgetp(L, LUA_REGISTRYINDEX, &command_registrations_key); + defer c.lua_settop(L, c.lua_gettop(L) - 1); + + const n: usize = @intCast(c.lua_rawlen(L, -1)); + if (n == 0) return arena.alloc(CommandRegistration, 0) catch BridgeError.OutOfMemory; + + var out = arena.alloc(CommandRegistration, n) catch return BridgeError.OutOfMemory; + var i: usize = 1; + while (i <= n) : (i += 1) { + _ = c.lua_rawgeti(L, -1, @intCast(i)); + defer c.lua_settop(L, c.lua_gettop(L) - 1); + + out[i - 1] = .{ + .name = try readStringField(L, -1, "name", arena), + .description = try readStringField(L, -1, "description", arena), + }; + } + return out; +} + /// In an *invocation-mode* state (registrations table populated by re- /// running the script), push the handler function for `tool_name` onto the /// stack. Caller is responsible for popping it after use. @@ -287,6 +337,47 @@ fn registerToolThunk(L_opt: ?*c.lua_State) callconv(.c) c_int { return 0; } +/// Implementation of `panto.register_command { name=, description=, handler= }`. +/// +/// Expects a single table argument with three named fields. The handler +/// is a function `function(args) ... end` where `args` is the trimmed +/// remainder of the slash-command line (a string, possibly empty). Its +/// return value is ignored — commands act by side effect, like the +/// builtin Zig commands. Appends a `{ name, description, handler }` +/// record to the command registrations table. +fn registerCommandThunk(L_opt: ?*c.lua_State) callconv(.c) c_int { + const L = L_opt.?; + c.luaL_checktype(L, 1, T_TABLE); + + // Stack after these pulls: + // 1: args table (input) + // 2: name (string) + // 3: description (string) + // 4: handler (function) + expectFieldNamed(L, 1, "name", T_STRING, "register_command"); + expectFieldNamed(L, 1, "description", T_STRING, "register_command"); + expectFieldNamed(L, 1, "handler", T_FUNCTION, "register_command"); + + // Build the record table { name, description, handler }. + c.lua_createtable(L, 0, 3); + c.lua_pushvalue(L, 2); + c.lua_setfield(L, -2, "name"); + c.lua_pushvalue(L, 3); + c.lua_setfield(L, -2, "description"); + c.lua_pushvalue(L, 4); // handler + c.lua_setfield(L, -2, "handler"); + + // Append to the command registrations table. + _ = c.lua_rawgetp(L, LUA_REGISTRYINDEX, &command_registrations_key); + // Stack: ..., record, regs_table + const n: c_int = @intCast(c.lua_rawlen(L, -1)); + c.lua_pushvalue(L, -2); // copy record above regs_table + c.lua_rawseti(L, -2, n + 1); + c.lua_settop(L, c.lua_gettop(L) - 1); // pop regs_table + + return 0; +} + /// Push `args_table[field_name]` onto the stack and assert it has the /// expected type. Raises a Lua error if missing or wrong type. fn expectField( @@ -294,13 +385,26 @@ fn expectField( args_idx: c_int, comptime field_name: [:0]const u8, expected_type: c_int, +) void { + expectFieldNamed(L, args_idx, field_name, expected_type, "register_tool"); +} + +/// Like `expectField`, but lets the caller name the bridge function in +/// the error message (`register_tool` vs `register_command`). +fn expectFieldNamed( + L: *c.lua_State, + args_idx: c_int, + comptime field_name: [:0]const u8, + expected_type: c_int, + comptime fn_name: [:0]const u8, ) void { _ = c.lua_getfield(L, args_idx, field_name.ptr); const got = c.lua_type(L, -1); if (got != expected_type) { + const fmt: [:0]const u8 = fn_name ++ ": field '%s' must be %s (got %s)"; _ = c.luaL_error( L, - "register_tool: field '%s' must be %s (got %s)", + fmt.ptr, field_name.ptr, c.lua_typename(L, expected_type), c.lua_typename(L, got), @@ -476,6 +580,58 @@ test "install creates panto.register_tool global" { try std.testing.expectEqual(@as(c_int, T_TABLE), c.lua_type(L, -1)); _ = c.lua_getfield(L, -1, "register_tool"); try std.testing.expectEqual(@as(c_int, T_FUNCTION), c.lua_type(L, -1)); + _ = c.lua_getglobal(L, "panto"); + _ = c.lua_getfield(L, -1, "register_command"); + try std.testing.expectEqual(@as(c_int, T_FUNCTION), c.lua_type(L, -1)); +} + +test "register_command records name and description" { + const L = c.luaL_newstate() orelse return error.LuaInitFailed; + defer c.lua_close(L); + c.luaL_openlibs(L); + install(L); + + const script = + \\panto.register_command { + \\ name = "hello", + \\ description = "Greets the user.", + \\ handler = function(args) return "hi " .. args end, + \\} + ; + if (c.luaL_loadstring(L, script) != 0 or c.lua_pcallk(L, 0, 0, 0, 0, null) != 0) { + var len: usize = 0; + const msg = c.lua_tolstring(L, -1, &len); + std.debug.print("lua error: {s}\n", .{msg[0..len]}); + return error.LuaScriptFailed; + } + + var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena_state.deinit(); + const cmds = try harvestCommandRegistrations(L, arena_state.allocator()); + + try std.testing.expectEqual(@as(usize, 1), cmds.len); + try std.testing.expectEqualStrings("hello", cmds[0].name); + try std.testing.expectEqualStrings("Greets the user.", cmds[0].description); +} + +test "register_command rejects a non-function handler" { + const L = c.luaL_newstate() orelse return error.LuaInitFailed; + defer c.lua_close(L); + c.luaL_openlibs(L); + install(L); + + const script = + \\panto.register_command { + \\ name = "bad", description = "d", handler = 42, + \\} + ; + const loaded = c.luaL_loadstring(L, script) == 0; + try std.testing.expect(loaded); + // Should raise a Lua error (non-zero pcall result). + try std.testing.expect(c.lua_pcallk(L, 0, 0, 0, 0, null) != 0); + var len: usize = 0; + const msg = c.lua_tolstring(L, -1, &len); + try std.testing.expect(std.mem.indexOf(u8, msg[0..len], "register_command") != null); } test "register_tool records name, description, schema_json" { -- cgit v1.3