summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/lua_bridge.zig163
-rw-r--r--src/lua_event_bridge.zig20
-rw-r--r--src/lua_runtime.zig83
-rw-r--r--src/luarocks_runtime.zig40
-rw-r--r--src/main.zig7
-rw-r--r--src/system_prompt.zig15
6 files changed, 263 insertions, 65 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")
diff --git a/src/lua_event_bridge.zig b/src/lua_event_bridge.zig
index 1fbc82a..f6d67e0 100644
--- a/src/lua_event_bridge.zig
+++ b/src/lua_event_bridge.zig
@@ -738,6 +738,7 @@ test "Lua on-handler fires through the native bus into Lua" {
defer c.lua_close(L);
c.luaL_openlibs(L);
lua_bridge.install(L);
+ lua_bridge.installTestPantoTable(L);
var bridge = EventBridge.init(testing.allocator, L);
defer bridge.deinit();
@@ -770,6 +771,7 @@ test "Lua-defined component renders lines through the bridged vtable" {
defer c.lua_close(L);
c.luaL_openlibs(L);
lua_bridge.install(L);
+ lua_bridge.installTestPantoTable(L);
var bridge = EventBridge.init(testing.allocator, L);
defer bridge.deinit();
@@ -807,6 +809,7 @@ test "bridged component render truncates to the width contract" {
defer c.lua_close(L);
c.luaL_openlibs(L);
lua_bridge.install(L);
+ lua_bridge.installTestPantoTable(L);
var bridge = EventBridge.init(testing.allocator, L);
defer bridge.deinit();
@@ -835,6 +838,7 @@ test "bridged component render error yields a safe fallback line, not a crash" {
defer c.lua_close(L);
c.luaL_openlibs(L);
lua_bridge.install(L);
+ lua_bridge.installTestPantoTable(L);
var bridge = EventBridge.init(testing.allocator, L);
defer bridge.deinit();
@@ -862,6 +866,7 @@ test "wrap pattern: Lua reads the native default, wraps it, and renders through
defer c.lua_close(L);
c.luaL_openlibs(L);
lua_bridge.install(L);
+ lua_bridge.installTestPantoTable(L);
var bridge = EventBridge.init(testing.allocator, L);
defer bridge.deinit();
@@ -933,6 +938,7 @@ test "skills-style claim-by-name (§7.5) works end-to-end through the Lua bridge
defer c.lua_close(L);
c.luaL_openlibs(L);
lua_bridge.install(L);
+ lua_bridge.installTestPantoTable(L);
var bridge = EventBridge.init(testing.allocator, L);
defer bridge.deinit();
@@ -1008,6 +1014,7 @@ test "native-passthrough get/set round-trip: setComponent(getComponent()) keeps
defer c.lua_close(L);
c.luaL_openlibs(L);
lua_bridge.install(L);
+ lua_bridge.installTestPantoTable(L);
var bridge = EventBridge.init(testing.allocator, L);
defer bridge.deinit();
@@ -1056,6 +1063,7 @@ test "Lua emit drives the native bus (custom event reaches a native handler)" {
defer c.lua_close(L);
c.luaL_openlibs(L);
lua_bridge.install(L);
+ lua_bridge.installTestPantoTable(L);
var bridge = EventBridge.init(testing.allocator, L);
defer bridge.deinit();
@@ -1093,6 +1101,7 @@ test "bridged render: a long error on a NARROW width still satisfies the width c
defer c.lua_close(L);
c.luaL_openlibs(L);
lua_bridge.install(L);
+ lua_bridge.installTestPantoTable(L);
var bridge = EventBridge.init(testing.allocator, L);
defer bridge.deinit();
@@ -1125,6 +1134,7 @@ test "bridged render: non-array / nil / non-string returns each yield a safe fal
defer c.lua_close(L);
c.luaL_openlibs(L);
lua_bridge.install(L);
+ lua_bridge.installTestPantoTable(L);
var bridge = EventBridge.init(testing.allocator, L);
defer bridge.deinit();
@@ -1162,6 +1172,7 @@ test "bridged render: empty array renders zero lines (no fallback)" {
defer c.lua_close(L);
c.luaL_openlibs(L);
lua_bridge.install(L);
+ lua_bridge.installTestPantoTable(L);
var bridge = EventBridge.init(testing.allocator, L);
defer bridge.deinit();
@@ -1189,6 +1200,7 @@ test "bridged render: UTF-8 line truncates on codepoint boundaries to the width"
defer c.lua_close(L);
c.luaL_openlibs(L);
lua_bridge.install(L);
+ lua_bridge.installTestPantoTable(L);
var bridge = EventBridge.init(testing.allocator, L);
defer bridge.deinit();
@@ -1222,6 +1234,7 @@ test "bridged firstLineChanged is cache-derived: append stays near the tail" {
defer c.lua_close(L);
c.luaL_openlibs(L);
lua_bridge.install(L);
+ lua_bridge.installTestPantoTable(L);
var bridge = EventBridge.init(testing.allocator, L);
defer bridge.deinit();
@@ -1270,6 +1283,7 @@ test "bridged firstLineChanged: a mid-line replace reports the changed line, shr
defer c.lua_close(L);
c.luaL_openlibs(L);
lua_bridge.install(L);
+ lua_bridge.installTestPantoTable(L);
var bridge = EventBridge.init(testing.allocator, L);
defer bridge.deinit();
@@ -1317,6 +1331,7 @@ test "bridged handleInput round-trips: a Lua method mutates state the next rende
defer c.lua_close(L);
c.luaL_openlibs(L);
lua_bridge.install(L);
+ lua_bridge.installTestPantoTable(L);
var bridge = EventBridge.init(testing.allocator, L);
defer bridge.deinit();
@@ -1363,6 +1378,7 @@ test "bridged component: invalidate frees the ref+cache eagerly; teardown is lea
defer c.lua_close(L);
c.luaL_openlibs(L);
lua_bridge.install(L);
+ lua_bridge.installTestPantoTable(L);
var bridge = EventBridge.init(testing.allocator, L);
defer bridge.deinit(); // frees all tracked BridgedComponents
@@ -1396,6 +1412,7 @@ test "lifecycle payload fields marshal to Lua: tool id/delta/input/output, think
defer c.lua_close(L);
c.luaL_openlibs(L);
lua_bridge.install(L);
+ lua_bridge.installTestPantoTable(L);
var bridge = EventBridge.init(testing.allocator, L);
defer bridge.deinit();
@@ -1453,6 +1470,7 @@ test "lifecycle handlers fire for every new event name" {
defer c.lua_close(L);
c.luaL_openlibs(L);
lua_bridge.install(L);
+ lua_bridge.installTestPantoTable(L);
var bridge = EventBridge.init(testing.allocator, L);
defer bridge.deinit();
@@ -1506,6 +1524,7 @@ test "claim-by-name at tool_details swaps over the start default; releasing the
defer c.lua_close(L);
c.luaL_openlibs(L);
lua_bridge.install(L);
+ lua_bridge.installTestPantoTable(L);
var bridge = EventBridge.init(testing.allocator, L);
defer bridge.deinit();
@@ -1613,6 +1632,7 @@ test "intra-emit clobber: two handlers each mint a Lua component in ONE emit; bo
defer c.lua_close(L);
c.luaL_openlibs(L);
lua_bridge.install(L);
+ lua_bridge.installTestPantoTable(L);
var bridge = EventBridge.init(testing.allocator, L);
defer bridge.deinit();
diff --git a/src/lua_runtime.zig b/src/lua_runtime.zig
index e26d017..2574fae 100644
--- a/src/lua_runtime.zig
+++ b/src/lua_runtime.zig
@@ -130,6 +130,15 @@ pub const LuaRuntime = struct {
// Install the real `panto.ext.emit` now that the bridge exists, so
// a Lua `emit(name, data)` reaches the native bus once attached.
lua_bridge.installEmit(L, @ptrCast(eb), lua_event_bridge.emitThunk);
+
+ // In production, `require('panto')` is wired to the native module
+ // by `installPantoModule` (after bootstrap stages `panto.so`).
+ // Unit tests have no staged `.so`, and load extension scripts that
+ // `require('panto')` for `panto.ext.*`; give them a fabricated
+ // `{ ext = ... }` module table so those `require`s resolve.
+ if (@import("builtin").is_test) {
+ lua_bridge.installTestPantoTable(L);
+ }
return self;
}
@@ -139,6 +148,62 @@ pub const LuaRuntime = struct {
return self.event_bridge;
}
+ /// Wire `require('panto')` to the native `libpanto-lua` module plus the
+ /// CLI's `ext` subtable. Run **after** luarocks bootstrap has staged
+ /// `panto.so` and configured `package.cpath`, and **before**
+ /// `installScheduler` (whose `_record_result` / wrapper closure live on
+ /// the module table).
+ ///
+ /// The sequence (all in Zig, via the C API, so the CLI never calls
+ /// `luaopen_panto` itself — it only speaks `require`):
+ ///
+ /// 1. `require('panto')` resolves the staged `panto.so` on `cpath`,
+ /// running its `luaopen_panto`, which returns a **fresh** table
+ /// (the agent/stream surface).
+ /// 2. Attach the `ext` subtable (built by `lua_bridge.install`) onto
+ /// that fresh native table — mutating the host's own copy, which
+ /// is safe precisely because the table is fresh, not shared.
+ /// 3. Install a `package.preload['panto']` loader returning that same
+ /// augmented table, so any *later* `require('panto')` (e.g. from
+ /// an extension) gets `ext` too. `require` already cached the
+ /// table in `package.loaded` from step 1; the preload entry wins
+ /// over `cpath` for any cache miss.
+ /// 4. Stash the augmented table under `panto_table_key` so internal
+ /// Zig (`pushPantoTable`, the scheduler) reaches it.
+ pub fn installPantoModule(self: *LuaRuntime) !void {
+ const L = self.L;
+
+ // 1. require('panto') -> native agent/stream table on the stack.
+ _ = c.lua_getglobal(L, "require");
+ _ = c.lua_pushlstring(L, "panto", 5);
+ if (c.lua_pcallk(L, 1, 1, 0, 0, null) != 0) {
+ logTopAsError(L, "panto-lua: require('panto') failed (was panto.so staged on cpath?)");
+ c.lua_settop(L, c.lua_gettop(L) - 1);
+ return RuntimeError.LuaInitFailed;
+ }
+ if (c.lua_type(L, -1) != lua_bridge.T_TABLE) {
+ c.lua_settop(L, c.lua_gettop(L) - 1);
+ return RuntimeError.LuaInitFailed;
+ }
+ // Stack: ..., panto (native, fresh)
+
+ // 2. panto.ext = <ext table>.
+ lua_bridge.pushExtTable(L);
+ if (c.lua_type(L, -1) != lua_bridge.T_TABLE) {
+ c.lua_settop(L, c.lua_gettop(L) - 2);
+ return RuntimeError.LuaInitFailed;
+ }
+ c.lua_setfield(L, -2, "ext"); // panto.ext = ext; pops ext
+ // Stack: ..., panto (augmented)
+
+ // 3. Set package.preload['panto'] to return this augmented table.
+ lua_bridge.installPantoPreload(L); // leaves `panto` on the stack
+
+ // 4. Stash in the registry slot for internal Zig access.
+ lua_bridge.setPantoTable(L); // dups + stores; leaves `panto`
+ c.lua_settop(L, c.lua_gettop(L) - 1); // pop the leftover `panto`
+ }
+
/// Install the libuv-driven coroutine scheduler:
/// - Register `panto._record_result` (C function with `self` as
/// light-userdata upvalue) so the wrapper closure can hand
@@ -148,7 +213,9 @@ pub const LuaRuntime = struct {
/// - Cache `require("luv").run` for fast per-tick access.
///
/// Must be called after luarocks bootstrap has installed luv,
- /// otherwise the `require("luv")` step will fail.
+ /// otherwise the `require("luv")` step will fail. Also after
+ /// `installPantoModule`, since the scheduler writes onto the module
+ /// table.
pub fn installScheduler(self: *LuaRuntime) !void {
try installRecordResult(self);
try installWrapperClosure(self);
@@ -261,15 +328,15 @@ pub const LuaRuntime = struct {
//
// Push `panto.ext.register_tool` *first*, then load+run the chunk
// so its return value naturally lands above it; calling pcall then
- // consumes both in the right order.
+ // consumes both in the right order. We reach `register_tool`
+ // through the `ext` table directly (it exists from `install`,
+ // independent of whether the native module table has been wired).
const L = self.L;
- lua_bridge.pushPantoTable(L);
- _ = c.lua_getfield(L, -1, "ext");
+ lua_bridge.pushExtTable(L);
_ = c.lua_getfield(L, -1, "register_tool");
- // Stack: ..., panto, ext, register_tool. Collapse to just
- // register_tool by overwriting the lowest of the three.
- c.lua_copy(L, -1, -3); // overwrite `panto` with `register_tool`
- c.lua_settop(L, c.lua_gettop(L) - 2); // pop ext + duplicate
+ // Stack: ..., ext, register_tool. Collapse to just register_tool.
+ c.lua_copy(L, -1, -2); // overwrite `ext` with `register_tool`
+ c.lua_settop(L, c.lua_gettop(L) - 1); // pop duplicate
// Stack: ..., register_tool
if (c.luaL_loadfilex(L, path_z.ptr, null) != 0) {
diff --git a/src/luarocks_runtime.zig b/src/luarocks_runtime.zig
index e652096..7f01c19 100644
--- a/src/luarocks_runtime.zig
+++ b/src/luarocks_runtime.zig
@@ -38,6 +38,7 @@ const panto_home = @import("panto_home.zig");
const embedded_luarocks = @import("embedded_luarocks");
const embedded_lua_headers = @import("embedded_lua_headers");
const embedded_agent = @import("embedded_agent");
+const embedded_panto_so = @import("embedded_panto_so");
const lua_bridge = @import("lua_bridge.zig");
const c = lua_bridge.c;
@@ -137,6 +138,7 @@ pub fn bootstrap(
try panto_home.ensureDirsExist(layout, io);
try stageLuaHeaders(allocator, io, layout);
try stageAgentTree(allocator, io, layout);
+ try stagePantoModule(allocator, io, layout);
try stageDefaultConfig(allocator, io, layout);
const lua_wrapper_path = try writeLuaWrapper(allocator, io, layout, panto_executable_path);
defer allocator.free(lua_wrapper_path);
@@ -203,6 +205,33 @@ fn stageLuaHeaders(
}
// ---------------------------------------------------------------------------
+// Stage the embedded native `panto.so`
+// ---------------------------------------------------------------------------
+
+/// Write the binary-embedded `libpanto-lua` shared object to
+/// `<tree>/lib/lua/<short>/panto.so`, which is already on `package.cpath`
+/// (see `configurePackagePaths`). This guarantees the embedded VM's
+/// `require('panto')` resolves to the native agent/stream module on a
+/// cold, network-less machine — the module is *ours* and version-locked
+/// to this binary's Lua, so unlike luv it cannot be fetched from luarocks.
+///
+/// The `.so` leaves `lua_*` undefined and resolves them against the host
+/// binary at `dlopen` time, identical to how a luarocks-installed C rock
+/// (e.g. luv) works — `exe.rdynamic = true` keeps those symbols findable.
+///
+/// Uses the same `writeIfDifferent` discipline as `stageLuaHeaders` so an
+/// unchanged `.so` keeps its on-disk mtime across reruns.
+fn stagePantoModule(
+ allocator: Allocator,
+ io: Io,
+ layout: panto_home.Layout,
+) !void {
+ var dir = try Io.Dir.cwd().openDir(io, layout.lib_lua_dir, .{});
+ defer dir.close(io);
+ try writeIfDifferent(allocator, io, dir, "panto.so", embedded_panto_so.bytes);
+}
+
+// ---------------------------------------------------------------------------
// Stage the embedded `agent/` tree
// ---------------------------------------------------------------------------
@@ -311,11 +340,20 @@ fn writeIfDifferent(
name: []const u8,
contents: []const u8,
) !void {
- if (dir.readFileAlloc(io, name, allocator, .limited(1 << 22))) |existing| {
+ // Size the read limit to the new content (plus a small margin) so it
+ // never truncates the comparison — staged files range from tiny
+ // headers to a multi-megabyte `panto.so`. A larger on-disk file can't
+ // match `contents` anyway, so capping at `contents.len + 1` is enough
+ // to detect inequality without reading unbounded bytes.
+ const limit = contents.len + 1;
+ if (dir.readFileAlloc(io, name, allocator, .limited(limit))) |existing| {
defer allocator.free(existing);
if (std.mem.eql(u8, existing, contents)) return;
} else |err| switch (err) {
error.FileNotFound => {},
+ // A file larger than our limit definitionally differs from
+ // `contents`; fall through and rewrite.
+ error.StreamTooLong => {},
else => return err,
}
try dir.writeFile(io, .{ .sub_path = name, .data = contents });
diff --git a/src/main.zig b/src/main.zig
index 34a687a..06fd29e 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -355,6 +355,13 @@ pub fn main(init: std.process.Init) !void {
);
}
+ // Bootstrap staged `panto.so` onto the embedded VM's cpath and
+ // configured `package.path`/`cpath`; now wire `require('panto')` to
+ // the native module + the CLI's `ext` subtable. Must run before
+ // `installScheduler` (which writes onto the module table) and before
+ // any extension loads (which `require('panto')`).
+ try rt.installPantoModule();
+
// luv is installed (or already present) at this point; wire the
// libuv-driven coroutine scheduler before any extensions get a
// chance to register tools that might want to yield.
diff --git a/src/system_prompt.zig b/src/system_prompt.zig
index 6676442..9dce3e5 100644
--- a/src/system_prompt.zig
+++ b/src/system_prompt.zig
@@ -330,6 +330,15 @@ fn projectLayerDir(arena: Allocator, io: Io) ![]u8 {
const testing = std.testing;
+/// Test helper: append a single-text user message (the `addUserMessage`
+/// signature now takes a block slice).
+fn addUserText(conv: *panto.Conversation, text: []const u8) !void {
+ const tb = try panto.textualBlockFromSlice(conv.allocator, text);
+ var block: panto.ContentBlock = .{ .Text = tb };
+ errdefer block.deinit(conv.allocator);
+ try conv.addUserMessage(&.{block});
+}
+
test "blocksEqual - length and content" {
try testing.expect(blocksEqual(&.{ "a", "b" }, &.{ "a", "b" }));
try testing.expect(!blocksEqual(&.{ "a", "b" }, &.{ "a", "c" }));
@@ -344,7 +353,7 @@ test "effectiveConfigWindow - no replace anchors to leading system blocks" {
try conv.addSystemMessage("seed");
try conv.addSystemMessage("append-1");
- try conv.addUserMessage("hi");
+ try addUserText(&conv, "hi");
var arena_state = std.heap.ArenaAllocator.init(alloc);
defer arena_state.deinit();
@@ -361,10 +370,10 @@ test "effectiveConfigWindow - anchors to last replace block" {
defer conv.deinit();
try conv.addSystemMessage("old seed");
- try conv.addUserMessage("hi");
+ try addUserText(&conv, "hi");
try conv.replaceSystemMessage("new seed");
try conv.addSystemMessage("new append");
- try conv.addUserMessage("again");
+ try addUserText(&conv, "again");
var arena_state = std.heap.ArenaAllocator.init(alloc);
defer arena_state.deinit();