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}