Skip to main content

nomiscript/
host_fn.rs

1//! Caller-supplied native function specifications.
2//!
3//! The compiler accepts a list of [`HostFnSpec`] entries (rpc/cli/tui all
4//! provide their own) and treats them as first-class native callables: the
5//! corresponding wasm imports get registered when CompileContext is built in
6//! eval mode, and the native dispatcher emits a `(call $idx)` whenever a
7//! nomiscript form references the spec's `nomi_name`. The matching host-side
8//! linker registration is the consumer's responsibility — see
9//! `rpc::natives::link` for the rpc layer's mirror.
10
11use crate::ast::WasmType;
12
13/// Description of a single host fn the caller wants nomiscript code to be
14/// able to invoke.
15///
16/// `result: Option<WasmType>` is the single source of truth for the wasm
17/// import signature. The compiler reads it via
18/// `CompileContext::host_import_return_type` and declares the import
19/// using the abstract heap type (`(ref null struct)` / `(ref null array)`)
20/// that matches wasmtime's `Rooted<StructRef>` / `Rooted<ArrayRef>`
21/// `WasmTy::valtype()`; a `ref.cast` after every call recovers the
22/// concrete type. No per-shape flags, no synthesized guest-side wrap
23/// helpers — host returns the GC ref directly.
24#[derive(Debug, Clone)]
25pub struct HostFnSpec {
26    /// Symbol the lisp source uses, e.g. `rpc-protocol-version`. Stored
27    /// uppercase to match the reader's symbol-normalisation behaviour.
28    pub nomi_name: String,
29    /// Wasm import module — typically `"nomi"` for the rpc-eval channel.
30    pub import_module: String,
31    /// Wasm import function name — typically the snake_case rpc fn name.
32    pub import_name: String,
33    /// Parameter wasm types (in declaration order).
34    pub params: Vec<WasmType>,
35    /// Return type, `None` for `() -> ()` host fns.
36    pub result: Option<WasmType>,
37}
38
39impl HostFnSpec {
40    #[must_use]
41    pub fn new(
42        nomi_name: impl Into<String>,
43        import_module: impl Into<String>,
44        import_name: impl Into<String>,
45    ) -> Self {
46        Self {
47            nomi_name: nomi_name.into().to_ascii_uppercase(),
48            import_module: import_module.into(),
49            import_name: import_name.into(),
50            params: Vec::new(),
51            result: None,
52        }
53    }
54
55    #[must_use]
56    pub fn returns(mut self, ty: WasmType) -> Self {
57        self.result = Some(ty);
58        self
59    }
60
61    #[must_use]
62    pub fn with_params(mut self, params: Vec<WasmType>) -> Self {
63        self.params = params;
64        self
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn nomi_name_is_normalised_to_uppercase() {
74        let spec = HostFnSpec::new("rpc-protocol-version", "nomi", "rpc_protocol_version");
75        assert_eq!(spec.nomi_name, "RPC-PROTOCOL-VERSION");
76    }
77
78    #[test]
79    fn returns_and_params_are_optional() {
80        let bare = HostFnSpec::new("foo", "nomi", "foo");
81        assert!(bare.params.is_empty());
82        assert!(bare.result.is_none());
83
84        let with_ret = HostFnSpec::new("bar", "nomi", "bar").returns(WasmType::I32);
85        assert_eq!(with_ret.result, Some(WasmType::I32));
86
87        let with_args =
88            HostFnSpec::new("baz", "nomi", "baz").with_params(vec![WasmType::I32, WasmType::Ratio]);
89        assert_eq!(with_args.params, vec![WasmType::I32, WasmType::Ratio]);
90    }
91}