summaryrefslogtreecommitdiff
path: root/src/lua_bridge.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/lua_bridge.zig')
-rw-r--r--src/lua_bridge.zig163
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")