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

            
11
use 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)]
25
pub 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

            
39
impl HostFnSpec {
40
    #[must_use]
41
87588
    pub fn new(
42
87588
        nomi_name: impl Into<String>,
43
87588
        import_module: impl Into<String>,
44
87588
        import_name: impl Into<String>,
45
87588
    ) -> Self {
46
87588
        Self {
47
87588
            nomi_name: nomi_name.into().to_ascii_uppercase(),
48
87588
            import_module: import_module.into(),
49
87588
            import_name: import_name.into(),
50
87588
            params: Vec::new(),
51
87588
            result: None,
52
87588
        }
53
87588
    }
54

            
55
    #[must_use]
56
511770
    pub fn returns(mut self, ty: WasmType) -> Self {
57
511770
        self.result = Some(ty);
58
511770
        self
59
511770
    }
60

            
61
    #[must_use]
62
371009
    pub fn with_params(mut self, params: Vec<WasmType>) -> Self {
63
371009
        self.params = params;
64
371009
        self
65
371009
    }
66
}
67

            
68
#[cfg(test)]
69
mod tests {
70
    use super::*;
71

            
72
    #[test]
73
1
    fn nomi_name_is_normalised_to_uppercase() {
74
1
        let spec = HostFnSpec::new("rpc-protocol-version", "nomi", "rpc_protocol_version");
75
1
        assert_eq!(spec.nomi_name, "RPC-PROTOCOL-VERSION");
76
1
    }
77

            
78
    #[test]
79
1
    fn returns_and_params_are_optional() {
80
1
        let bare = HostFnSpec::new("foo", "nomi", "foo");
81
1
        assert!(bare.params.is_empty());
82
1
        assert!(bare.result.is_none());
83

            
84
1
        let with_ret = HostFnSpec::new("bar", "nomi", "bar").returns(WasmType::I32);
85
1
        assert_eq!(with_ret.result, Some(WasmType::I32));
86

            
87
1
        let with_args =
88
1
            HostFnSpec::new("baz", "nomi", "baz").with_params(vec![WasmType::I32, WasmType::Ratio]);
89
1
        assert_eq!(with_args.params, vec![WasmType::I32, WasmType::Ratio]);
90
1
    }
91
}