diff options
Diffstat (limited to 'src/lua_bridge.zig')
| -rw-r--r-- | src/lua_bridge.zig | 163 |
1 files changed, 110 insertions, 53 deletions
diff --git a/src/lua_bridge.zig b/src/lua_bridge.zig index 9ee5089..6395293 100644 --- a/src/lua_bridge.zig +++ b/src/lua_bridge.zig @@ -102,12 +102,24 @@ pub var command_registrations_key: u8 = 0; 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 +/// `LUA_REGISTRYINDEX`. 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. +/// +/// This slot is populated by the runtime's `installPantoModule`, which +/// obtains the **native** `panto` table via `require('panto')` (resolving +/// the staged `panto.so` on `cpath`) and attaches `ext` to it. Until then +/// the slot is empty and `pushPantoTable` pushes `nil`; nothing fetches it +/// before module load. pub var panto_table_key: u8 = 0; +/// The key under which we stash the `panto.ext` extension subtable in +/// `LUA_REGISTRYINDEX`. Built by `install` (before any native module +/// exists) so the host can attach it to the native `panto` table once +/// `require('panto')` resolves. Distinct from `panto_table_key` because +/// `ext` is constructed first and outlives the module-load step. +pub var ext_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. @@ -137,41 +149,68 @@ pub const OnRegistration = struct { // Public bridge API // --------------------------------------------------------------------------- -/// 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')`. +/// Push the canonical `panto` module table onto the stack from its +/// registry slot. The slot is populated by the runtime's +/// `installPantoModule` (which `require`s the native module and attaches +/// `ext`); before that runs the slot is empty and this pushes `nil`. +/// 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. +/// Push the `panto.ext` extension subtable from its registry slot. Built +/// by `install`, so it is available before the native module loads (the +/// host attaches it to the native table in `installPantoModule`). +pub fn pushExtTable(L: *c.lua_State) void { + _ = c.lua_rawgetp(L, LUA_REGISTRYINDEX, &ext_table_key); +} + +/// Stash a `panto` module table (on the stack top) into `panto_table_key`. +/// Called by `installPantoModule` after it builds the native+`ext` table. +/// Does NOT pop — leaves the table on the stack for the caller. +pub fn setPantoTable(L: *c.lua_State) void { + c.lua_pushvalue(L, -1); // dup for the registry slot + c.lua_rawsetp(L, LUA_REGISTRYINDEX, &panto_table_key); +} + +/// Register a `package.preload['panto']` loader returning the `panto` +/// module table currently on the stack top. Does NOT pop the table — +/// leaves it in place for the caller. The loader returns the table stashed +/// under `panto_table_key`, so it must be called together with +/// `setPantoTable` (the runtime does both in `installPantoModule`). +pub fn installPantoPreload(L: *c.lua_State) void { + _ = c.lua_getglobal(L, "package"); + _ = c.lua_getfield(L, -1, "preload"); + c.lua_pushcclosure(L, pantoPreloadThunk, 0); + c.lua_setfield(L, -2, "panto"); // preload.panto = thunk + c.lua_settop(L, c.lua_gettop(L) - 2); // pop preload + package +} + +/// The `package.preload['panto']` loader: returns the augmented `panto` +/// table from `panto_table_key`. Standard preload-loader contract: one +/// return value. Any `require('panto')` after `installPantoModule` — from +/// an extension or otherwise — routes here (preload is searcher slot 1). 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')`. +/// Build the `panto.ext` extension subtable and the registry tables that +/// hold harvested tool/command/event-handler registrations. /// -/// 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`. +/// This does **not** build the `panto` module table and does **not** touch +/// `package.preload` — the `panto` table comes from the native +/// `libpanto-lua` module via `require('panto')`, wired by the runtime's +/// `installPantoModule` once the staged `panto.so` is on `cpath`. Here we +/// only construct `ext` (stashed under `ext_table_key`) so the host can +/// attach it to that native table later. /// /// All extension-authoring APIs (`register_tool`, `register_command`, -/// `on`, `emit`) live under `panto.ext`; the bare `panto` table is -/// reserved for the future libpanto API. `emit` is installed here as a +/// `on`, `emit`) live under `panto.ext`. `emit` is installed here as a /// safe no-op default; the long-lived runtime overrides it with a closure -/// carrying its context via `installEmit` so a Lua `emit` can drive the -/// native `EventBus`. -/// -/// Also creates the registry tables that hold harvested tool, command, and -/// event-handler registrations. +/// carrying its context via `installEmit` once the module table exists. pub fn install(L: *c.lua_State) void { // Create the registrations table: an array of records, each shaped // { name=, description=, schema_json=, handler= }. @@ -187,8 +226,7 @@ pub fn install(L: *c.lua_State) void { c.lua_createtable(L, 0, 0); c.lua_rawsetp(L, LUA_REGISTRYINDEX, &on_registrations_key); - // Build the `panto` global as { ext = { ... } }. - c.lua_createtable(L, 0, 1); // panto + // Build the `panto.ext` subtable and stash it for later attachment. c.lua_createtable(L, 0, 4); // panto.ext c.lua_pushcclosure(L, registerToolThunk, 0); c.lua_setfield(L, -2, "register_tool"); @@ -199,27 +237,8 @@ pub fn install(L: *c.lua_State) void { // Default `emit`: a no-op until the runtime installs the real one. c.lua_pushcclosure(L, emitNoopThunk, 0); c.lua_setfield(L, -2, "emit"); - // panto.ext = <ext table> - c.lua_setfield(L, -2, "ext"); - - // 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 + // Stash `ext` under its registry key, consuming it. + c.lua_rawsetp(L, LUA_REGISTRYINDEX, &ext_table_key); } /// Override `panto.ext.emit` with a closure that carries `ctx` as a @@ -235,12 +254,11 @@ pub fn installEmit( ctx: *anyopaque, emit_fn: *const fn (L_opt: ?*c.lua_State) callconv(.c) c_int, ) void { - pushPantoTable(L); - _ = c.lua_getfield(L, -1, "ext"); + pushExtTable(L); c.lua_pushlightuserdata(L, ctx); c.lua_pushcclosure(L, emit_fn, 1); c.lua_setfield(L, -2, "emit"); - c.lua_settop(L, c.lua_gettop(L) - 2); // pop ext + panto + c.lua_settop(L, c.lua_gettop(L) - 1); // pop ext } /// The default `panto.ext.emit`: a no-op. Replaced by `installEmit` once @@ -562,6 +580,24 @@ fn readHandlerResultTable( return .{ .items = items }; } +/// Test-only: fabricate a minimal `panto` module table `{ ext = <ext> }` +/// and wire `require('panto')` to it, mirroring what the runtime's +/// `installPantoModule` does in production (minus the native agent/stream +/// surface, which needs a staged `panto.so` unavailable in unit tests). +/// +/// Production code never calls this — it `require`s the real native +/// module. Unit tests that load extension scripts (which `require('panto')` +/// for `panto.ext.*`) call `install(L)` then this, so those scripts +/// resolve without a `.so` on disk. +pub fn installTestPantoTable(L: *c.lua_State) void { + c.lua_createtable(L, 0, 1); // panto + pushExtTable(L); + c.lua_setfield(L, -2, "ext"); // panto.ext = ext + installPantoPreload(L); // preload.panto -> panto_table_key + setPantoTable(L); // stash; leaves panto on stack + c.lua_settop(L, c.lua_gettop(L) - 1); // pop panto +} + // --------------------------------------------------------------------------- // Lua-callable C functions // --------------------------------------------------------------------------- @@ -885,24 +921,38 @@ fn readStringField( // Tests // --------------------------------------------------------------------------- -test "install creates panto.ext table with register_tool/register_command/on/emit" { +test "install creates the ext table with register_tool/register_command/on/emit" { const L = c.luaL_newstate() orelse return error.LuaInitFailed; defer c.lua_close(L); c.luaL_openlibs(L); install(L); - // No `panto` global: it is delivered via `require('panto')`. + // No `panto` global, and `install` alone does NOT build the module + // table — the native module supplies it via `require('panto')`, and + // the host attaches `ext` in `installPantoModule`. So before that + // wiring the module slot is empty. 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_NIL), c.lua_type(L, -1)); + c.lua_settop(L, c.lua_gettop(L) - 1); + + // The `ext` table exists and carries the four authoring functions. + pushExtTable(L); try std.testing.expectEqual(@as(c_int, T_TABLE), c.lua_type(L, -1)); inline for (.{ "register_tool", "register_command", "on", "emit" }) |field| { _ = c.lua_getfield(L, -1, field); try std.testing.expectEqual(@as(c_int, T_FUNCTION), c.lua_type(L, -1)); c.lua_settop(L, c.lua_gettop(L) - 1); } + + // After the test wiring, `require('panto')` resolves to a `{ ext }` + // module table. + installTestPantoTable(L); + 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)); } test "panto.ext.on records event registrations in order" { @@ -910,6 +960,7 @@ test "panto.ext.on records event registrations in order" { defer c.lua_close(L); c.luaL_openlibs(L); install(L); + installTestPantoTable(L); const script = \\local panto = require("panto") @@ -959,6 +1010,7 @@ test "register_command records name and description" { defer c.lua_close(L); c.luaL_openlibs(L); install(L); + installTestPantoTable(L); const script = \\local panto = require("panto") @@ -989,6 +1041,7 @@ test "register_command rejects a non-function handler" { defer c.lua_close(L); c.luaL_openlibs(L); install(L); + installTestPantoTable(L); const script = \\local panto = require("panto") @@ -1010,6 +1063,7 @@ test "register_tool records name, description, schema_json" { defer c.lua_close(L); c.luaL_openlibs(L); install(L); + installTestPantoTable(L); const script = \\local panto = require("panto") @@ -1044,6 +1098,7 @@ test "handler invocation: input parsed, result captured" { defer c.lua_close(L); c.luaL_openlibs(L); install(L); + installTestPantoTable(L); const script = \\local panto = require("panto") @@ -1080,6 +1135,7 @@ test "readHandlerResult: table with text and attachments" { defer c.lua_close(L); c.luaL_openlibs(L); install(L); + installTestPantoTable(L); const script = \\return { @@ -1109,6 +1165,7 @@ test "handler crash: error message surfaces via xpcall traceback hook" { defer c.lua_close(L); c.luaL_openlibs(L); install(L); + installTestPantoTable(L); const script = \\local panto = require("panto") |
