Lines
100 %
Functions
50 %
Branches
//! Caller-supplied native function specifications.
//!
//! The compiler accepts a list of [`HostFnSpec`] entries (rpc/cli/tui all
//! provide their own) and treats them as first-class native callables: the
//! corresponding wasm imports get registered when CompileContext is built in
//! eval mode, and the native dispatcher emits a `(call $idx)` whenever a
//! nomiscript form references the spec's `nomi_name`. The matching host-side
//! linker registration is the consumer's responsibility — see
//! `rpc::natives::link` for the rpc layer's mirror.
use crate::ast::WasmType;
/// Description of a single host fn the caller wants nomiscript code to be
/// able to invoke.
///
/// `result: Option<WasmType>` is the single source of truth for the wasm
/// import signature. The compiler reads it via
/// `CompileContext::host_import_return_type` and declares the import
/// using the abstract heap type (`(ref null struct)` / `(ref null array)`)
/// that matches wasmtime's `Rooted<StructRef>` / `Rooted<ArrayRef>`
/// `WasmTy::valtype()`; a `ref.cast` after every call recovers the
/// concrete type. No per-shape flags, no synthesized guest-side wrap
/// helpers — host returns the GC ref directly.
#[derive(Debug, Clone)]
pub struct HostFnSpec {
/// Symbol the lisp source uses, e.g. `rpc-protocol-version`. Stored
/// uppercase to match the reader's symbol-normalisation behaviour.
pub nomi_name: String,
/// Wasm import module — typically `"nomi"` for the rpc-eval channel.
pub import_module: String,
/// Wasm import function name — typically the snake_case rpc fn name.
pub import_name: String,
/// Parameter wasm types (in declaration order).
pub params: Vec<WasmType>,
/// Return type, `None` for `() -> ()` host fns.
pub result: Option<WasmType>,
}
impl HostFnSpec {
#[must_use]
pub fn new(
nomi_name: impl Into<String>,
import_module: impl Into<String>,
import_name: impl Into<String>,
) -> Self {
Self {
nomi_name: nomi_name.into().to_ascii_uppercase(),
import_module: import_module.into(),
import_name: import_name.into(),
params: Vec::new(),
result: None,
pub fn returns(mut self, ty: WasmType) -> Self {
self.result = Some(ty);
self
pub fn with_params(mut self, params: Vec<WasmType>) -> Self {
self.params = params;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nomi_name_is_normalised_to_uppercase() {
let spec = HostFnSpec::new("rpc-protocol-version", "nomi", "rpc_protocol_version");
assert_eq!(spec.nomi_name, "RPC-PROTOCOL-VERSION");
fn returns_and_params_are_optional() {
let bare = HostFnSpec::new("foo", "nomi", "foo");
assert!(bare.params.is_empty());
assert!(bare.result.is_none());
let with_ret = HostFnSpec::new("bar", "nomi", "bar").returns(WasmType::I32);
assert_eq!(with_ret.result, Some(WasmType::I32));
let with_args =
HostFnSpec::new("baz", "nomi", "baz").with_params(vec![WasmType::I32, WasmType::Ratio]);
assert_eq!(with_args.params, vec![WasmType::I32, WasmType::Ratio]);