diff options
| author | t <t@tjp.lol> | 2026-06-10 09:46:00 -0600 |
|---|---|---|
| committer | t <t@tjp.lol> | 2026-06-10 09:55:05 -0600 |
| commit | 9bf1d800f2a24ed71221c64370c98ee8d907314b (patch) | |
| tree | d1b77ac9140e57ba523e97a4b815475ddba2c04e /src | |
| parent | 938db5f476184b2d4f613e04cce60a5191581d7f (diff) | |
expose `panto` to extensions as a require()able module, not a global
Diffstat (limited to 'src')
| -rw-r--r-- | src/extension_loader.zig | 5 | ||||
| -rw-r--r-- | src/lua_bridge.zig | 72 | ||||
| -rw-r--r-- | src/lua_event_bridge.zig | 7 | ||||
| -rw-r--r-- | src/lua_runtime.zig | 19 |
4 files changed, 92 insertions, 11 deletions
diff --git a/src/extension_loader.zig b/src/extension_loader.zig index 8a735bf..0ab8091 100644 --- a/src/extension_loader.zig +++ b/src/extension_loader.zig @@ -627,6 +627,7 @@ test "loadFromDirs: project shadows user end-to-end (via long-lived runtime)" { try makeDir(tmp.dir, "project_ext"); try writeFile(tmp.dir, "user_ext/greet.lua", + \\local panto = require("panto") \\panto.ext.register_tool { \\ name = "greet", description = "user version", \\ schema = { type = "object" }, @@ -634,6 +635,7 @@ test "loadFromDirs: project shadows user end-to-end (via long-lived runtime)" { \\} ); try writeFile(tmp.dir, "project_ext/greet.lua", + \\local panto = require("panto") \\panto.ext.register_tool { \\ name = "greet", description = "project version", \\ schema = { type = "object" }, @@ -674,6 +676,7 @@ test "loadFromDirs: tool-name collision between extensions errors" { try makeDir(tmp.dir, "ext"); try writeFile(tmp.dir, "ext/alpha.lua", + \\local panto = require("panto") \\panto.ext.register_tool { \\ name = "clash", description = "a", \\ schema = { type = "object" }, @@ -681,6 +684,7 @@ test "loadFromDirs: tool-name collision between extensions errors" { \\} ); try writeFile(tmp.dir, "ext/beta.lua", + \\local panto = require("panto") \\panto.ext.register_tool { \\ name = "clash", description = "b", \\ schema = { type = "object" }, @@ -941,6 +945,7 @@ test "loadFromDirs: extension and tool share a *file* name independently" { try makeDir(tmp.dir, "tools"); try writeFile(tmp.dir, "ext/foo.lua", + \\local panto = require("panto") \\panto.ext.register_tool { \\ name = "ext_foo", description = "e", \\ schema = { type = "object" }, 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" }, diff --git a/src/lua_event_bridge.zig b/src/lua_event_bridge.zig index b445072..1fbc82a 100644 --- a/src/lua_event_bridge.zig +++ b/src/lua_event_bridge.zig @@ -720,7 +720,12 @@ fn harvestOnInto(bridge: *EventBridge) !void { } fn runScript(L: *c.lua_State, src: [:0]const u8) !void { - if (c.luaL_loadstring(L, src.ptr) != 0 or c.lua_pcallk(L, 0, 0, 0, 0, null) != 0) { + // `panto` is not a global; extensions reach it via `require('panto')`. + // Prepend the require so these bridge tests can keep using `panto.ext` + // without each script repeating the boilerplate. + var buf: [8192]u8 = undefined; + const wrapped = std.fmt.bufPrintZ(&buf, "local panto = require(\"panto\")\n{s}", .{src}) catch return error.LuaScriptFailed; + if (c.luaL_loadstring(L, wrapped.ptr) != 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]}); diff --git a/src/lua_runtime.zig b/src/lua_runtime.zig index 797b97b..e26d017 100644 --- a/src/lua_runtime.zig +++ b/src/lua_runtime.zig @@ -263,7 +263,7 @@ pub const LuaRuntime = struct { // so its return value naturally lands above it; calling pcall then // consumes both in the right order. const L = self.L; - _ = c.lua_getglobal(L, "panto"); + lua_bridge.pushPantoTable(L); _ = c.lua_getfield(L, -1, "ext"); _ = c.lua_getfield(L, -1, "register_tool"); // Stack: ..., panto, ext, register_tool. Collapse to just @@ -974,7 +974,7 @@ fn invokeCoroutineSync( /// stores into the matching slot. fn installRecordResult(self: *LuaRuntime) !void { const L = self.L; - _ = c.lua_getglobal(L, "panto"); + lua_bridge.pushPantoTable(L); if (c.lua_type(L, -1) != lua_bridge.T_TABLE) { c.lua_settop(L, c.lua_gettop(L) - 1); return RuntimeError.LuaInitFailed; @@ -1038,7 +1038,10 @@ fn recordResultC(L: ?*c.lua_State) callconv(.c) c_int { /// Stored in the Lua registry under `self.wrapper_ref`. fn installWrapperClosure(self: *LuaRuntime) !void { const L = self.L; + // `panto` is not a global, so the wrapper closes over the table + // (passed as an argument) rather than looking it up globally. const snippet = + \\local panto = ... \\return function(idx, handler, input) \\ local ok, val = pcall(handler, input) \\ panto._record_result(idx, ok, val) @@ -1049,7 +1052,9 @@ fn installWrapperClosure(self: *LuaRuntime) !void { c.lua_settop(L, c.lua_gettop(L) - 1); return RuntimeError.LuaInitFailed; } - if (c.lua_pcallk(L, 0, 1, 0, 0, null) != 0) { + // Pass the `panto` table as the chunk's vararg argument. + lua_bridge.pushPantoTable(L); + if (c.lua_pcallk(L, 1, 1, 0, 0, null) != 0) { logTopAsError(L, "panto-lua: wrapper closure failed to evaluate"); c.lua_settop(L, c.lua_gettop(L) - 1); return RuntimeError.LuaInitFailed; @@ -1153,7 +1158,12 @@ fn freeResults(results: []panto.ToolCallResult) void { } fn writeTempScript(dir: Io.Dir, name: []const u8, source: []const u8) ![]const u8 { - try dir.writeFile(testing.io, .{ .sub_path = name, .data = source }); + // `panto` is not a global; extensions reach it via `require('panto')`. + // Prepend the require so these tests exercise the real preload path + // without each script repeating the boilerplate. + var src_buf: [16384]u8 = undefined; + const data = try std.fmt.bufPrint(&src_buf, "local panto = require(\"panto\")\n{s}", .{source}); + try dir.writeFile(testing.io, .{ .sub_path = name, .data = data }); var buf: [std.fs.max_path_bytes]u8 = undefined; const n = try dir.realPathFile(testing.io, name, &buf); return testing.allocator.dupe(u8, buf[0..n]); @@ -1428,6 +1438,7 @@ test "directory-style extension can require sibling modules" { try tmp.dir.writeFile(testing.io, .{ .sub_path = "ext/init.lua", .data = + \\local panto = require("panto") \\local util = require("util") \\panto.ext.register_tool { \\ name = "shout", description = "uppercase + bang", |
