# Zig Plugins & Stdlib Namespaces All Deft stdlib functions — `str`, `echo`, `http/get`, `tui/split`, `sqlite/open` — are written in Zig and discovered at build time, with no central manifest to edit. This document covers the plugin architecture, how pseudo-namespaces work, and how to add a new plugin. ## Architecture ``` ┌──────────────────────────────────────────────────────────────┐ │ Build time │ │ │ │ build.zig ─► gen_plugin_registry.zig │ │ │ │ walks src/host/plugins/** + src/host/stdext/* │ │ looks for `pub const info` in every .zig file │ │ writes plugin_registry.gen.zig (only if changed) │ │ │ │ Each plugin file exports one public constant: │ │ pub const info: PluginInfo = ...; │ │ │ │ Optional feature gates: │ │ pub const requires_tui: bool = true; (TUI-only) │ │ pub const requires_ffi: bool = true; (FFI-only) │ └──────────────────────┬───────────────────────────────────────┘ │ @import ┌──────────────────────▼───────────────────────────────────────┐ │ VM init │ │ │ │ stdlib.registerAll(alloc, heap, &vm.globals); │ │ stdlib.registerAll(alloc, heap, &vm.core_ns.vars); │ │ │ │ Each PluginEntry becomes an ObjNativeFunc stored as a │ │ flat string key in globals and core_ns: │ │ globals["fs/read"] = │ │ globals["fs/write"] = │ │ globals["string/upper"] = │ └──────────────────────────────────────────────────────────────┘ ``` ## The PluginInfo Contract Every plugin file exports a single `PluginInfo` struct: ```zig pub const info: PluginInfo = ...; ``` Its fields: | Field | Type | Purpose | |---|---|---| | `name` | `[]const u8` | Plugin identifier, e.g. `"string"`, `"fs"`, `"sqlite"` | | `version` | `u32` | API version (default `1`) | | `entries` | `[]const PluginEntry` | The function table | Each `PluginEntry` describes one stdlib function: | Field | Type | Purpose | |---|---|---| | `name` | `[]const u8` | Fully-qualified name, e.g. `"fs/read"`, `"string/trim"` | | `arity` | `i16` | Fixed argument count, or `-1` for variadic | | `func` | `NativeFn` | The function pointer | | `flags` | `FnFlags` | `.needs_vm = true` for HOFs, coroutines, I/O | | `doc` | `?[]const u8` | Help string | | `params` | `?[]const ParamDoc` | Per-parameter docs | | `returns` | `?[]const u8` | Return value description | | `flag_opts_npos` | `u16` | CLI-style `--flag value` support (0 = off) | ### Function signatures Two signatures are supported, determined by the entry's `flags.needs_vm`: ```zig // Pure functions — FnFlags.needs_vm is false pub const NativeFn = *const fn (*DeftCtx, []const Value) Value; // VM-aware — FnFlags.needs_vm is true (HOFs, coroutines, I/O) pub const VmNativeFn = *const fn (*anyopaque, []const Value) Value; ``` The `DeftCtx` passed to every stdlib function provides: - **Allocation**: `.alloc`, `.heap` - **VM/runtime access**: `.vm`, `.runtime` (typed cast via `.vmAs(T)`, `.runtimeAs(T)`) - **Arg extraction**: `.str(i, args)`, `.num(i, args)`, `.int(i, args)`, `.boolean(i, args)`, `.kw(i, args)` - **Value construction**: `.newString(s)`, `.newNumber(n)`, `.newBool(b)`, `.newKeyword(k)` - **Result construction**: `.ok(v)` → `%Ok`, `.err(msg)` / `.errVal(v)` → `%Err` ## Comptime Convenience: `plugin.decl()` and `plugin.derive()` Instead of hand-writing `PluginEntry` structs and manual `[]const Value` unpacking, most plugins use two comptime macros that auto-derive arity, type-check arg extraction, and build the registration table: ```zig const plugin = @import("../../runtime/plugin.zig"); const DeftCtx = plugin.DeftCtx; const Value = plugin.Value; const d = plugin.decl; pub const info = plugin.derive("math", .{ d("sqrt", &nativeSqrt, .{ .doc = "Square root", .returns = "number" }), d("sin", &nativeSin, .{ .doc = "Sine of radians", .returns = "number" }), d("pow", &nativePow, .{ .doc = "Raise base to exponent", .params = &.{ .{ .name = "base" }, .{ .name = "exp" } }, .returns = "number", }), }); ``` `decl` recognizes two author signatures: - **Typed** — `fn(*DeftCtx, s: []const u8, n: i64) Value` → fixed arity derived from param count - **Raw** — `fn(*DeftCtx, []const Value) Value` → variadic (arity `-1`) Supported typed arg types: `[]const u8`, `f64`, `i64`, `bool`, `Value`. Return type may be `Value` or `!Value` (errors auto-convert to `%Err`). Max 4 typed params; for more, use the raw `[]const Value` form. `derive(name, entries)` builds a `PluginInfo` from a tuple of `PluginEntry` values, setting `.name` and embedding the entries array at comptime. ### Without the macros (raw form) For reference, the same plugin written manually: ```zig fn nativeSqrt(ctx: *DeftCtx, args: []const Value) Value { const x = ctx.num(0, args) orelse return ctx.err("expected number"); if (x < 0) return ctx.err("sqrt: negative argument"); return ctx.newNumber(std.math.sqrt(x)); } pub const info = plugin.PluginInfo{ .name = "math", .version = 1, .entries = &.{ .{ .name = "sqrt", .arity = 1, .func = &nativeSqrt, .doc = "Square root", .returns = "number" }, .{ .name = "sin", .arity = 1, .func = &nativeSin, .doc = "Sine of radians", .returns = "number" }, }, }; ``` The macros are preferred: less boilerplate, fewer manual `[]const Value` indices, and automatic arity derivation. ## How Pseudo-Namespaces Work Unlike Clojure-style namespaces (created by `defmod` in `.dft` files), plugin namespaces are **flat-key pseudo-namespaces**. There is no `Namespace` struct for `fs`, `tui`, or `sqlite`. The `/` in `"fs/read"` is purely a naming convention — each entry is stored as a flat string key: ``` globals["fs/read"] = // from fs plugin globals["fs/write"] = // from fs plugin globals["fs/home"] = // from fs plugin globals["tui/split"] = // from workspace plugin globals["tui/close"] = // from workspace plugin ``` When Deft code references `fs` as a bare name (e.g. to inspect it or pass it around), the VM **lazily synthesizes** a namespace map via `synthesizePluginMap`: 1. Scans all globals matching the prefix `"fs/"` 2. Strips the prefix to get short names: `read`, `write`, `home` 3. Builds a persistent map `{ :read , :write , :home }` 4. Stores it as `globals["fs"]` for future fast access This happens transparently — you never call `synthesizePluginMap` yourself. The VM does it on first bare-name access. ### Resolution path for a qualified call When code calls `[fs/read "file.txt"]`: 1. The bytecode compiler emits a `load "fs/read"` instruction 2. `resolveVar("fs/read")` checks `globals` → finds the `ObjNativeFunc` 3. `dispatchCall` invokes the native function pointer directly ## Feature Gating Plugins can be gated behind build-time feature flags: ```zig pub const requires_tui: bool = true; // Only included in retro/retra builds pub const requires_ffi: bool = true; // Only included in FFI-enabled builds ``` If neither is set, the plugin is **always loaded** (common). The registry generator partitions plugins into three arrays — `common`, `tui_only`, `ffi_only` — and concatenates them at init time. Check feature availability at runtime:
 [capabilities]            # → map of all feature flags
[get [capabilities] :tui] # → true/false
[get [capabilities] :ffi] # → true/false 
## Build-Time Discovery Adding a new plugin requires **zero manual edits to any manifest**. Drop a file under `src/host/plugins/` or `src/host/stdext/` that exports `pub const info`, and the build system discovers it automatically: 1. `build.zig` calls `gen_plugin_registry.zig` during graph construction 2. The generator walks both directories recursively 3. For each `.zig` file containing `pub const info`, it reads the feature gates and emits an `@import` reference 4. The output `plugin_registry.gen.zig` is written only if content changed (avoids spurious recompilation) 5. The registry uses conditional compilation (`if (build_options.enable_tui)`) so TUI/FFI modules are never analyzed in builds that don't need them ## Adding a New Plugin ### Example: a `compress/` plugin Create `src/host/plugins/core/compress_plugin.zig`: ```zig const std = @import("std"); const plugin = @import("../../runtime/plugin.zig"); const DeftCtx = plugin.DeftCtx; const Value = plugin.Value; const d = plugin.decl; const value_mod = @import("../../../kernel/value/value.zig"); pub const info = plugin.derive("compress", .{ d("compress/gzip", &nativeGzip, .{ .doc = "Compress bytes with gzip", .params = &.{.{ .name = "data", .type_hint = "string" }}, .returns = "string", }), d("compress/decompress-gzip", &nativeGunzip, .{ .doc = "Decompress a gzip-compressed string", .params = &.{.{ .name = "data", .type_hint = "string" }}, .returns = "string", }), }); fn nativeGzip(ctx: *DeftCtx, args: []const Value) Value { const data = ctx.str(0, args) orelse return ctx.err("expected string"); // Compress with std.compress.gzip... return ctx.ok(ctx.newString(compressed_bytes)); } fn nativeGunzip(ctx: *DeftCtx, args: []const Value) Value { const data = ctx.str(0, args) orelse return ctx.err("expected string"); // Decompress with std.compress.gzip... return ctx.ok(ctx.newString(decompressed_bytes)); } ``` Rebuild. The functions `compress/gzip` and `compress/decompress-gzip` are now available in all Deft code. The `compress` bare name lazily synthesizes a map `{ :gzip , :decompress-gzip }`. ### TUI-only plugin example Add `pub const requires_tui = true`: ```zig pub const requires_tui: bool = true; ``` The plugin is only compiled into `retro`/`retra` builds. The scripting binary `deft` never sees its imports. ## Plugin Locations | Directory | Purpose | |---|---| | `src/host/plugins/core/` | Core language stdlib functions (string, math, types, list, map, set, etc.) | | `src/host/plugins/data/` | Data structure stdlib functions (array, list, map, set) | | `src/host/plugins/system/` | System stdlib functions (config, io, log, pubsub, rpc, runtime, task) | | `src/host/plugins/` (root) | TUI, dialog, debug, capabilities, workspace, draw, editor, etc. | | `src/host/stdext/` | Standard library (fs, http, json, sqlite, os, net, crypto, etc.) | The split is convention, not enforcement. Any file under either tree is discovered automatically. ## Error Convention Stdlib functions return errors as `%Err` tagged maps. Use the `DeftCtx` helpers: ```zig fn nativeFn(ctx: *DeftCtx, args: []const Value) Value { const s = ctx.str(0, args) orelse return ctx.err("expected string"); // ... do work ... return ctx.ok(result); // → %{ :type :ok/value :value result } } ``` For raw-arity functions returning `!Value` via `decl`, errors auto-convert: ```zig fn nativeOpen(ctx: *DeftCtx, path: []const u8) !Value { const f = try std.fs.cwd().openFile(path, .{}); defer f.close(); // ... return ctx.newString(content); } // On error → ctx.err(@errorName(e)) automatically ``` ## Namespace Naming Conventions - **Short name** with no `/` for frequently-used standalone functions: `str`, `trim`, `echo`, `map`, `filter`, `nil?` - **Namespace-slash** for logically grouped operations: `fs/read`, `fs/write`, `http/get`, `sqlite/query`, `tui/split` - The slash is just a naming convention. No real namespace struct exists for the plugin prefix — the VM lazily synthesizes one on first bare-name access. ## Limits - **Max 4 typed params** via `plugin.decl()`. Use the raw `[]const Value` form for variadic or higher-arity functions. - **Name collisions** between plugins are not detected at build time. If two plugins register `"foo/bar"`, the second registration silently overwrites the first. - **Dynamic plugin loading** (`.so`-based) is planned but not yet shipped. All plugins are currently statically linked.