diff options
Diffstat (limited to 'src/lua_bridge.zig')
| -rw-r--r-- | src/lua_bridge.zig | 72 |
1 files changed, 66 insertions, 6 deletions
diff --git a/src/lua_bridge.zig b/src/lua_bridge.zig index 4446ca7..9ee5089 100644 --- a/src/lua_bridge.zig +++ b/src/lua_bridge.zig @@ -1,6 +1,9 @@ //! Lua C-API bridge for the panto CLI. //! -//! Exposes a `panto` global table inside any `lua_State` we construct. +//! Exposes a `panto` table inside any `lua_State` we construct, reachable +//! from Lua via `require('panto')` (registered into `package.preload` — +//! there is no `panto` global). Internal Zig code reaches the same table +//! through the registry via `pushPantoTable`. //! All extension-authoring APIs live under the `panto.ext` subtable; the //! bare `panto` namespace is reserved for the future full libpanto API //! surface, so extensions never collide with it. The current `panto.ext` @@ -98,6 +101,13 @@ pub var command_registrations_key: u8 = 0; /// handler that bridges back into Lua. pub var on_registrations_key: u8 = 0; +/// The key under which we stash the canonical `panto` module table in +/// `LUA_REGISTRYINDEX`. The `package.preload['panto']` loader returns this +/// table, and internal Zig setup (`installEmit`, the runtime's +/// `_record_result` / wrapper / register_tool paths) fetches it via +/// `pushPantoTable` instead of a global lookup — `panto` is not a global. +pub var panto_table_key: u8 = 0; + /// A single declared tool, as harvested from a script's top-level call to /// `panto.ext.register_tool`. All slices reference Lua-owned strings on the /// state's stack/registry; copy them before closing the state. @@ -127,8 +137,31 @@ pub const OnRegistration = struct { // Public bridge API // --------------------------------------------------------------------------- -/// Install the `panto` global (with its `panto.ext` extension subtable) -/// into the given state. +/// Push the canonical `panto` table (built by `install`) onto the stack +/// from its registry slot. Internal Zig setup uses this instead of a +/// global lookup, since `panto` is not a global — it is delivered to Lua +/// only through `require('panto')`. +pub fn pushPantoTable(L: *c.lua_State) void { + _ = c.lua_rawgetp(L, LUA_REGISTRYINDEX, &panto_table_key); +} + +/// The `package.preload['panto']` loader. Returns the canonical `panto` +/// table so `require('panto')` yields it (and `panto.ext`) without a +/// global. Standard preload-loader contract: one return value. +fn pantoPreloadThunk(L_opt: ?*c.lua_State) callconv(.c) c_int { + const L = L_opt.?; + pushPantoTable(L); + return 1; +} + +/// Install the `panto` table (with its `panto.ext` extension subtable) +/// into the given state, reachable from Lua via `require('panto')`. +/// +/// There is no `panto` global. The table is stashed in the registry +/// (`panto_table_key`) and a loader is registered into +/// `package.preload['panto']`, so any `require('panto')` — from an +/// extension or otherwise — returns it. Internal Zig code reaches the +/// same table via `pushPantoTable`. /// /// All extension-authoring APIs (`register_tool`, `register_command`, /// `on`, `emit`) live under `panto.ext`; the bare `panto` table is @@ -168,7 +201,25 @@ pub fn install(L: *c.lua_State) void { c.lua_setfield(L, -2, "emit"); // panto.ext = <ext table> c.lua_setfield(L, -2, "ext"); - c.lua_setglobal(L, "panto"); + + // Stash the `panto` table in the registry (keeps internal Zig access + // working without a global), then register the preload loader so + // `require('panto')` returns it. The table is still on the stack top. + c.lua_pushvalue(L, -1); // dup `panto` for the registry slot + c.lua_rawsetp(L, LUA_REGISTRYINDEX, &panto_table_key); + // Stack: ..., panto. Register the preload loader, consuming `panto`. + installPreloadLoader(L); +} + +/// Register `pantoPreloadThunk` into `package.preload['panto']`. Consumes +/// the `panto` table currently on the stack top (pops it). +fn installPreloadLoader(L: *c.lua_State) void { + c.lua_settop(L, c.lua_gettop(L) - 1); // pop the leftover `panto` + _ = c.lua_getglobal(L, "package"); + _ = c.lua_getfield(L, -1, "preload"); + c.lua_pushcclosure(L, pantoPreloadThunk, 0); + c.lua_setfield(L, -2, "panto"); + c.lua_settop(L, c.lua_gettop(L) - 2); // pop preload + package } /// Override `panto.ext.emit` with a closure that carries `ctx` as a @@ -184,7 +235,7 @@ pub fn installEmit( ctx: *anyopaque, emit_fn: *const fn (L_opt: ?*c.lua_State) callconv(.c) c_int, ) void { - _ = c.lua_getglobal(L, "panto"); + pushPantoTable(L); _ = c.lua_getfield(L, -1, "ext"); c.lua_pushlightuserdata(L, ctx); c.lua_pushcclosure(L, emit_fn, 1); @@ -840,7 +891,10 @@ test "install creates panto.ext table with register_tool/register_command/on/emi c.luaL_openlibs(L); install(L); - _ = c.lua_getglobal(L, "panto"); + // No `panto` global: it is delivered via `require('panto')`. + try std.testing.expectEqual(@as(c_int, T_NIL), c.lua_getglobal(L, "panto")); + c.lua_settop(L, c.lua_gettop(L) - 1); + pushPantoTable(L); try std.testing.expectEqual(@as(c_int, T_TABLE), c.lua_type(L, -1)); _ = c.lua_getfield(L, -1, "ext"); try std.testing.expectEqual(@as(c_int, T_TABLE), c.lua_type(L, -1)); @@ -858,6 +912,7 @@ test "panto.ext.on records event registrations in order" { install(L); const script = + \\local panto = require("panto") \\panto.ext.on("tool", function(e) end) \\panto.ext.on("assistant_text", function(e) end) \\panto.ext.on("tool", function(e) end) @@ -906,6 +961,7 @@ test "register_command records name and description" { install(L); const script = + \\local panto = require("panto") \\panto.ext.register_command { \\ name = "hello", \\ description = "Greets the user.", @@ -935,6 +991,7 @@ test "register_command rejects a non-function handler" { install(L); const script = + \\local panto = require("panto") \\panto.ext.register_command { \\ name = "bad", description = "d", handler = 42, \\} @@ -955,6 +1012,7 @@ test "register_tool records name, description, schema_json" { install(L); const script = + \\local panto = require("panto") \\panto.ext.register_tool { \\ name = "echo", \\ description = "Echoes its input back.", @@ -988,6 +1046,7 @@ test "handler invocation: input parsed, result captured" { install(L); const script = + \\local panto = require("panto") \\panto.ext.register_tool { \\ name = "echo", description = "echoes", \\ schema = { type = "object" }, @@ -1052,6 +1111,7 @@ test "handler crash: error message surfaces via xpcall traceback hook" { install(L); const script = + \\local panto = require("panto") \\panto.ext.register_tool { \\ name = "boom", description = "crashes", \\ schema = { type = "object" }, |
