Skip to main content

harn_hostlib/
registry.rs

1//! Registration plumbing.
2//!
3//! Each module exposes a [`HostlibCapability`] implementation that pushes
4//! its builtins into a [`BuiltinRegistry`]. The registry can then either
5//! be wired into a real [`harn_vm::Vm`] (production path) or introspected
6//! by tests to assert the exposed surface without touching the VM.
7
8use std::sync::Arc;
9
10use harn_vm::{Vm, VmError, VmValue};
11
12use crate::error::HostlibError;
13
14/// Sync builtin handler signature. Mirrors the closure type accepted by
15/// [`harn_vm::Vm::register_builtin`]; we keep it `Send + Sync` so capability
16/// instances can be shared across threads if an embedder ever wants that.
17pub type SyncHandler = Arc<dyn Fn(&[VmValue]) -> Result<VmValue, HostlibError> + Send + Sync>;
18
19/// One registered builtin. The name is what Harn scripts call (e.g.
20/// `hostlib_ast_parse_file`); `module` and `method` are the canonical
21/// schema-directory coordinates (`schemas/<module>/<method>.request.json`).
22#[derive(Clone)]
23pub struct RegisteredBuiltin {
24    /// Builtin name as Harn scripts see it.
25    pub name: &'static str,
26    /// Module bucket (e.g. `"ast"`, `"tools"`).
27    pub module: &'static str,
28    /// Method name within the module (e.g. `"parse_file"`, `"search"`).
29    pub method: &'static str,
30    /// Handler invoked when Harn calls the builtin.
31    pub handler: SyncHandler,
32}
33
34impl std::fmt::Debug for RegisteredBuiltin {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        f.debug_struct("RegisteredBuiltin")
37            .field("name", &self.name)
38            .field("module", &self.module)
39            .field("method", &self.method)
40            .finish()
41    }
42}
43
44/// Mutable collector each capability writes into during `register`.
45#[derive(Default)]
46pub struct BuiltinRegistry {
47    builtins: Vec<RegisteredBuiltin>,
48}
49
50impl BuiltinRegistry {
51    /// Construct an empty registry.
52    pub fn new() -> Self {
53        Self::default()
54    }
55
56    /// Push one builtin. Capabilities call this from `register_builtins`.
57    pub fn register(&mut self, builtin: RegisteredBuiltin) {
58        self.builtins.push(builtin);
59    }
60
61    /// Convenience: register a builtin whose body is the `unimplemented`
62    /// scaffold error.
63    pub fn register_unimplemented(
64        &mut self,
65        name: &'static str,
66        module: &'static str,
67        method: &'static str,
68    ) {
69        let handler: SyncHandler =
70            Arc::new(move |_args| Err(HostlibError::Unimplemented { builtin: name }));
71        self.register(RegisteredBuiltin {
72            name,
73            module,
74            method,
75            handler,
76        });
77    }
78
79    /// Convenience: register a stateless builtin backed by a plain fn
80    /// pointer. This is the shape almost every capability module uses;
81    /// keeping it here avoids each module hand-rolling its own copy.
82    pub(crate) fn register_fn(
83        &mut self,
84        module: &'static str,
85        name: &'static str,
86        method: &'static str,
87        runner: fn(&[VmValue]) -> Result<VmValue, HostlibError>,
88    ) {
89        let handler: SyncHandler = Arc::new(runner);
90        self.register(RegisteredBuiltin {
91            name,
92            module,
93            method,
94            handler,
95        });
96    }
97
98    /// Like [`Self::register_fn`], but wraps the handler in the shared
99    /// deterministic-tools permission gate
100    /// ([`crate::tools::permissions::gated_handler`]).
101    pub(crate) fn register_gated_fn(
102        &mut self,
103        module: &'static str,
104        name: &'static str,
105        method: &'static str,
106        runner: fn(&[VmValue]) -> Result<VmValue, HostlibError>,
107    ) {
108        self.register(RegisteredBuiltin {
109            name,
110            module,
111            method,
112            handler: crate::tools::permissions::gated_handler(name, runner),
113        });
114    }
115
116    /// Iterate over every registered builtin.
117    pub fn iter(&self) -> impl Iterator<Item = &RegisteredBuiltin> {
118        self.builtins.iter()
119    }
120
121    /// Total count.
122    pub fn len(&self) -> usize {
123        self.builtins.len()
124    }
125
126    /// True when nothing has been registered yet.
127    pub fn is_empty(&self) -> bool {
128        self.builtins.is_empty()
129    }
130
131    /// Look up a builtin by its Harn-visible name.
132    pub fn find(&self, name: &str) -> Option<&RegisteredBuiltin> {
133        self.builtins.iter().find(|b| b.name == name)
134    }
135}
136
137/// One module's worth of builtins. Kept tiny on purpose: capabilities exist
138/// purely so tests can reason about the surface without booting a VM, and
139/// so embedders can opt into individual modules.
140pub trait HostlibCapability: 'static {
141    /// Module name (matches the `schemas/<module>/` directory).
142    fn module_name(&self) -> &'static str;
143
144    /// Push every builtin this module exposes into `registry`.
145    fn register_builtins(&self, registry: &mut BuiltinRegistry);
146}
147
148/// Composes capabilities and emits VM registrations.
149///
150/// `HostlibRegistry` is the type embedders interact with. It owns the
151/// capability instances and the populated [`BuiltinRegistry`] together so
152/// the same surface can be inspected by tests *and* wired into a VM.
153pub struct HostlibRegistry {
154    builtins: BuiltinRegistry,
155    modules: Vec<&'static str>,
156}
157
158impl Default for HostlibRegistry {
159    fn default() -> Self {
160        Self::new()
161    }
162}
163
164impl HostlibRegistry {
165    /// Construct an empty registry. Most callers want [`crate::install_default`]
166    /// instead, which pre-populates every shipped capability.
167    pub fn new() -> Self {
168        Self {
169            builtins: BuiltinRegistry::new(),
170            modules: Vec::new(),
171        }
172    }
173
174    /// Add one capability to the registry. Returns `self` for chaining.
175    #[must_use]
176    pub fn with<C: HostlibCapability>(mut self, capability: C) -> Self {
177        let module = capability.module_name();
178        capability.register_builtins(&mut self.builtins);
179        self.modules.push(module);
180        self
181    }
182
183    /// Wire every registered builtin into the supplied VM.
184    pub fn register_into_vm(&mut self, vm: &mut Vm) {
185        for builtin in self.builtins.iter().cloned() {
186            let handler = builtin.handler.clone();
187            vm.register_builtin(
188                builtin.name,
189                move |args, _out| -> Result<VmValue, VmError> {
190                    handler(args).map_err(VmError::from)
191                },
192            );
193        }
194    }
195
196    /// Borrow the underlying [`BuiltinRegistry`] for introspection (e.g.
197    /// schema-drift tests).
198    pub fn builtins(&self) -> &BuiltinRegistry {
199        &self.builtins
200    }
201
202    /// List the module names that have been registered, in insertion order.
203    pub fn modules(&self) -> &[&'static str] {
204        &self.modules
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn unimplemented_builtins_route_through_error() {
214        let mut registry = BuiltinRegistry::new();
215        registry.register_unimplemented("hostlib_demo", "demo", "ping");
216        let entry = registry.find("hostlib_demo").expect("registered");
217        let err = (entry.handler)(&[]).expect_err("should be unimplemented");
218        assert!(
219            matches!(err, HostlibError::Unimplemented { builtin } if builtin == "hostlib_demo")
220        );
221    }
222
223    #[test]
224    fn registry_records_modules_in_order() {
225        struct First;
226        impl HostlibCapability for First {
227            fn module_name(&self) -> &'static str {
228                "first"
229            }
230            fn register_builtins(&self, _registry: &mut BuiltinRegistry) {}
231        }
232        struct Second;
233        impl HostlibCapability for Second {
234            fn module_name(&self) -> &'static str {
235                "second"
236            }
237            fn register_builtins(&self, _registry: &mut BuiltinRegistry) {}
238        }
239
240        let registry = HostlibRegistry::new().with(First).with(Second);
241        assert_eq!(registry.modules(), &["first", "second"]);
242    }
243}