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.zig72
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" },