From 97b10466d83d488ad2cb9e084159a445af74c845 Mon Sep 17 00:00:00 2001 From: t Date: Tue, 9 Jun 2026 00:00:56 -0600 Subject: event lifecycle --- src/lua_runtime.zig | 124 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 97 insertions(+), 27 deletions(-) (limited to 'src/lua_runtime.zig') diff --git a/src/lua_runtime.zig b/src/lua_runtime.zig index eb760f8..797b97b 100644 --- a/src/lua_runtime.zig +++ b/src/lua_runtime.zig @@ -29,9 +29,12 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const panto = @import("panto"); const lua_bridge = @import("lua_bridge.zig"); +const lua_event_bridge = @import("lua_event_bridge.zig"); +const ui_event = @import("tui_event.zig"); const c = lua_bridge.c; const Io = std.Io; +const EventBridge = lua_event_bridge.EventBridge; pub const SOURCE_NAME = "panto-lua"; @@ -67,7 +70,7 @@ pub const LuaRuntime = struct { /// the Lua registry (`luaL_ref` index). handlers: std.StringHashMap(c_int), - /// Slash commands declared by extensions via `panto.register_command`. + /// Slash commands declared by extensions via `panto.ext.register_command`. /// Owned by this runtime; the CLI reads these to build its command /// registry. Each entry's name/description point into `strings`. commands: std.array_list.Managed(LuaCommand), @@ -92,8 +95,14 @@ pub const LuaRuntime = struct { /// thread per source per turn). current_batch: ?*BatchState = null, + /// The extension UI event bridge (plan ยง7.6): owns Lua `on` handlers + /// and Lua-defined components, and drives the App's native `EventBus`. + /// Heap-allocated so its address is stable (the bridge stores `*self` + /// pointers in Lua light-userdata upvalues and handler ctx). + event_bridge: *EventBridge, + /// Create a new runtime. The `lua_State` is opened, standard libs - /// loaded, and the `panto.register_tool` bridge installed. + /// loaded, and the `panto.ext.register_tool` bridge installed. pub fn create(allocator: Allocator) !*LuaRuntime { const self = try allocator.create(LuaRuntime); errdefer allocator.destroy(self); @@ -103,6 +112,10 @@ pub const LuaRuntime = struct { c.luaL_openlibs(L); lua_bridge.install(L); + const eb = try allocator.create(EventBridge); + errdefer allocator.destroy(eb); + eb.* = EventBridge.init(allocator, L); + self.* = .{ .allocator = allocator, .L = L, @@ -111,10 +124,21 @@ pub const LuaRuntime = struct { .handlers = std.StringHashMap(c_int).init(allocator), .commands = std.array_list.Managed(LuaCommand).init(allocator), .command_handlers = std.StringHashMap(c_int).init(allocator), + .event_bridge = eb, }; + + // 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); return self; } + /// The extension UI event bridge. The embedder wires its `attachBus` + /// to the App's `EventBus` during startup (after the App exists). + pub fn eventBridge(self: *LuaRuntime) *EventBridge { + return self.event_bridge; + } + /// Install the libuv-driven coroutine scheduler: /// - Register `panto._record_result` (C function with `self` as /// light-userdata upvalue) so the wrapper closure can hand @@ -154,6 +178,11 @@ pub const LuaRuntime = struct { c.luaL_unref(self.L, lua_bridge.LUA_REGISTRYINDEX, self.uv_run_ref); } + // Free the UI event bridge (Lua handler/component refs + caches) + // BEFORE closing the state, since it unrefs registry entries. + self.event_bridge.deinit(); + self.allocator.destroy(self.event_bridge); + c.lua_close(self.L); self.decls.deinit(); @@ -168,7 +197,7 @@ pub const LuaRuntime = struct { /// `package_root`, if provided, is prepended to `package.path` so /// `require` finds sibling modules. /// - /// All `panto.register_tool` calls in the script run during this + /// All `panto.ext.register_tool` calls in the script run during this /// call. The runtime then harvests the registrations table, /// transfers handler functions into the Lua registry (one `luaL_ref` /// per tool), and records each tool's metadata in `self.decls`. @@ -204,10 +233,10 @@ pub const LuaRuntime = struct { } /// Load a single-tool Lua script and register the table it returns - /// as if `panto.register_tool` had been called on that table. + /// as if `panto.ext.register_tool` had been called on that table. /// /// The script's top-level chunk must return a table with the same - /// shape that `panto.register_tool` accepts: + /// shape that `panto.ext.register_tool` accepts: /// `{ name, description, schema, handler }`. This is the ergonomic /// form supported under a `tools/` directory. pub fn loadTool( @@ -230,14 +259,17 @@ pub const LuaRuntime = struct { // table). Use luaL_loadfilex + lua_pcallk directly so we can // ask for a return value (the bridge's loadFile discards them). // - // Push `panto.register_tool` *first*, then load+run the chunk so - // its return value naturally lands above it; calling pcall then + // 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. const L = self.L; _ = c.lua_getglobal(L, "panto"); + _ = c.lua_getfield(L, -1, "ext"); _ = c.lua_getfield(L, -1, "register_tool"); - c.lua_copy(L, -1, -2); // overwrite `panto` with `register_tool` - c.lua_settop(L, c.lua_gettop(L) - 1); // pop the duplicate + // 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: ..., register_tool if (c.luaL_loadfilex(L, path_z.ptr, null) != 0) { @@ -262,7 +294,7 @@ pub const LuaRuntime = struct { // Invoke register_tool(returned_table). Same validation, schema // serialization, and registrations-table append logic as an - // extension's `panto.register_tool` call. + // extension's `panto.ext.register_tool` call. if (c.lua_pcallk(L, 1, 0, 0, 0, null) != 0) { logTopAsError(L, "lua: register_tool failed for tool script"); return error.LuaRunFailed; @@ -336,6 +368,44 @@ pub const LuaRuntime = struct { } try self.harvestAndStoreCommands(); + try self.harvestAndStoreOnHandlers(); + } + + /// Walk the `panto.ext.on` registrations table populated by the script + /// just loaded. For each `{ event, handler }` record, hand the handler + /// function and event name to the `EventBridge`, which `luaL_ref`s the + /// function and remembers the subscription (in registration order). + /// Actual registration into the App's `EventBus` happens later via + /// `EventBridge.attachBus` (the bus may not exist at load time). + fn harvestAndStoreOnHandlers(self: *LuaRuntime) !void { + const L = self.L; + + _ = c.lua_rawgetp(L, lua_bridge.LUA_REGISTRYINDEX, &lua_bridge.on_registrations_key); + defer c.lua_settop(L, c.lua_gettop(L) - 1); + + const n: usize = @intCast(c.lua_rawlen(L, -1)); + var i: usize = 1; + while (i <= n) : (i += 1) { + _ = c.lua_rawgeti(L, -1, @intCast(i)); // push record + defer c.lua_settop(L, c.lua_gettop(L) - 1); // pop record + + // Read the event name (borrowed; the bridge dupes it). + _ = c.lua_getfield(L, -1, "event"); + var elen: usize = 0; + const eptr = c.lua_tolstring(L, -1, &elen); + const event_name = if (eptr != null) eptr[0..elen] else ""; + c.lua_settop(L, c.lua_gettop(L) - 1); // pop event string + + // Push the handler function and hand it to the bridge (which + // pushes a copy + luaL_refs it). + _ = c.lua_getfield(L, -1, "handler"); + if (c.lua_type(L, -1) != lua_bridge.T_FUNCTION) { + c.lua_settop(L, c.lua_gettop(L) - 1); // pop non-function + return RuntimeError.LuaHandlerNotFound; + } + try self.event_bridge.registerOnHandler(event_name, -1); + c.lua_settop(L, c.lua_gettop(L) - 1); // pop handler + } } /// Walk the command registrations table populated by the script just @@ -1094,7 +1164,7 @@ test "loadExtension records tool decls" { defer tmp.cleanup(); const source = - \\panto.register_tool { + \\panto.ext.register_tool { \\ name = "greet", description = "Says hi.", \\ schema = { type = "object", properties = { name = { type = "string" } } }, \\ handler = function(input) return "hi, " .. input.name end, @@ -1117,7 +1187,7 @@ test "loadExtension records command decls" { const source = \\_G.last_args = nil - \\panto.register_command { + \\panto.ext.register_command { \\ name = "shout", description = "Uppercases its args.", \\ handler = function(args) _G.last_args = args:upper() end, \\} @@ -1153,7 +1223,7 @@ test "runCommand surfaces a Lua error" { defer tmp.cleanup(); const source = - \\panto.register_command { + \\panto.ext.register_command { \\ name = "boom", description = "crashes", \\ handler = function(args) error("kaboom") end, \\} @@ -1180,8 +1250,8 @@ test "duplicate command name within runtime is rejected" { defer tmp.cleanup(); const source = - \\panto.register_command { name = "dup", description = "a", handler = function() end } - \\panto.register_command { name = "dup", description = "b", handler = function() end } + \\panto.ext.register_command { name = "dup", description = "a", handler = function() end } + \\panto.ext.register_command { name = "dup", description = "b", handler = function() end } ; const path = try writeTempScript(tmp.dir, "dup.lua", source); defer testing.allocator.free(path); @@ -1196,12 +1266,12 @@ test "invokeBatch runs each call through a coroutine and returns the result" { defer tmp.cleanup(); const source = - \\panto.register_tool { + \\panto.ext.register_tool { \\ name = "echo", description = "echoes", \\ schema = { type = "object", properties = { msg = { type = "string" } } }, \\ handler = function(input) return "got: " .. input.msg end, \\} - \\panto.register_tool { + \\panto.ext.register_tool { \\ name = "shout", description = "shouts", \\ schema = { type = "object", properties = { msg = { type = "string" } } }, \\ handler = function(input) return input.msg:upper() .. "!" end, @@ -1242,7 +1312,7 @@ test "module-global state survives across calls in the same runtime" { const source = \\local count = 0 - \\panto.register_tool { + \\panto.ext.register_tool { \\ name = "bump", description = "increment counter", \\ schema = { type = "object" }, \\ handler = function(input) @@ -1294,12 +1364,12 @@ test "handler crash: per-call error surfaces, sibling calls succeed" { defer tmp.cleanup(); const source = - \\panto.register_tool { + \\panto.ext.register_tool { \\ name = "ok", description = "ok", \\ schema = { type = "object" }, \\ handler = function(input) return "fine" end, \\} - \\panto.register_tool { + \\panto.ext.register_tool { \\ name = "boom", description = "bad", \\ schema = { type = "object" }, \\ handler = function(input) error("kaboom") end, @@ -1359,7 +1429,7 @@ test "directory-style extension can require sibling modules" { .sub_path = "ext/init.lua", .data = \\local util = require("util") - \\panto.register_tool { + \\panto.ext.register_tool { \\ name = "shout", description = "uppercase + bang", \\ schema = { type = "object", properties = { text = { type = "string" } } }, \\ handler = function(input) return util.shout(input.text) end, @@ -1393,7 +1463,7 @@ test "yielding handler with no event loop surfaces LuaHandlerYielded" { defer tmp.cleanup(); const source = - \\panto.register_tool { + \\panto.ext.register_tool { \\ name = "sleeper", description = "yields forever", \\ schema = { type = "object" }, \\ handler = function(input) coroutine.yield() ; return "never" end, @@ -1444,7 +1514,7 @@ test "scheduler: yielding handler is resumed by libuv" { const source = \\local uv = require("luv") - \\panto.register_tool { + \\panto.ext.register_tool { \\ name = "timer_say", description = "sleep then return", \\ schema = { type = "object" }, \\ handler = function(input) @@ -1521,12 +1591,12 @@ test "scheduler: handler that yields without arming libuv work is surfaced as an var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); const source = - \\panto.register_tool { + \\panto.ext.register_tool { \\ name = "abandon", description = "yields into the void", \\ schema = { type = "object" }, \\ handler = function() coroutine.yield(); return "never" end, \\} - \\panto.register_tool { + \\panto.ext.register_tool { \\ name = "fine", description = "sync ok", \\ schema = { type = "object" }, \\ handler = function() return "ok" end, @@ -1672,14 +1742,14 @@ test "loadExtension: duplicate tool name from a second extension errors" { defer tmp.cleanup(); const a = - \\panto.register_tool { + \\panto.ext.register_tool { \\ name = "clash", description = "a", \\ schema = { type = "object" }, \\ handler = function(input) return "a" end, \\} ; const b = - \\panto.register_tool { + \\panto.ext.register_tool { \\ name = "clash", description = "b", \\ schema = { type = "object" }, \\ handler = function(input) return "b" end, -- cgit v1.3