fusabi_host/
macros.rs

1//! Macros for typed host function registration.
2//!
3//! This module provides macros and helpers for ergonomically registering
4//! host functions with automatic value conversion and error handling.
5
6use crate::convert::{FromValue, IntoValue, ValueConversionError};
7use crate::engine::ExecutionContext;
8use crate::error::{Error, Result};
9use crate::value::Value;
10
11/// Trait for types that can be used as host function arguments.
12pub trait HostArg: Sized {
13    /// Extract this argument from the argument list at the given position.
14    fn extract(args: &[Value], position: usize) -> std::result::Result<Self, ArgError>;
15
16    /// Whether this argument is optional (has a default).
17    fn is_optional() -> bool {
18        false
19    }
20}
21
22/// Error extracting a host function argument.
23#[derive(Debug, Clone)]
24pub struct ArgError {
25    /// Argument position (0-indexed).
26    pub position: usize,
27    /// Expected type.
28    pub expected: &'static str,
29    /// What went wrong.
30    pub message: String,
31}
32
33impl std::fmt::Display for ArgError {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        write!(
36            f,
37            "argument {}: expected {}, {}",
38            self.position, self.expected, self.message
39        )
40    }
41}
42
43impl std::error::Error for ArgError {}
44
45impl From<ArgError> for Error {
46    fn from(err: ArgError) -> Self {
47        Error::HostFunction(err.to_string())
48    }
49}
50
51// Implement HostArg for common types
52
53impl<T: FromValue> HostArg for T {
54    fn extract(args: &[Value], position: usize) -> std::result::Result<Self, ArgError> {
55        let value = args.get(position).cloned().ok_or_else(|| ArgError {
56            position,
57            expected: std::any::type_name::<T>(),
58            message: "missing argument".to_string(),
59        })?;
60
61        T::from_value(value).map_err(|e| ArgError {
62            position,
63            expected: std::any::type_name::<T>(),
64            message: e.to_string(),
65        })
66    }
67}
68
69/// Wrapper for optional arguments.
70pub struct Optional<T>(pub Option<T>);
71
72impl<T: FromValue> HostArg for Optional<T> {
73    fn extract(args: &[Value], position: usize) -> std::result::Result<Self, ArgError> {
74        match args.get(position) {
75            None | Some(Value::Null) => Ok(Optional(None)),
76            Some(value) => {
77                let converted = T::from_value(value.clone()).map_err(|e| ArgError {
78                    position,
79                    expected: std::any::type_name::<T>(),
80                    message: e.to_string(),
81                })?;
82                Ok(Optional(Some(converted)))
83            }
84        }
85    }
86
87    fn is_optional() -> bool {
88        true
89    }
90}
91
92/// Wrapper for variadic arguments (rest parameters).
93pub struct Rest<T>(pub Vec<T>);
94
95impl<T: FromValue> HostArg for Rest<T> {
96    fn extract(args: &[Value], position: usize) -> std::result::Result<Self, ArgError> {
97        let rest: std::result::Result<Vec<T>, _> = args[position..]
98            .iter()
99            .enumerate()
100            .map(|(i, v)| {
101                T::from_value(v.clone()).map_err(|e| ArgError {
102                    position: position + i,
103                    expected: std::any::type_name::<T>(),
104                    message: e.to_string(),
105                })
106            })
107            .collect();
108        rest.map(Rest)
109    }
110
111    fn is_optional() -> bool {
112        true
113    }
114}
115
116/// Trait for host function return types.
117pub trait HostReturn {
118    /// Convert this return value to a Result<Value>.
119    fn into_result(self) -> Result<Value>;
120}
121
122// Value implements IntoValue, so the generic impl below handles it
123impl<T: IntoValue> HostReturn for T {
124    fn into_result(self) -> Result<Value> {
125        Ok(self.into_value())
126    }
127}
128
129impl<T: IntoValue, E: std::fmt::Display> HostReturn for std::result::Result<T, E> {
130    fn into_result(self) -> Result<Value> {
131        match self {
132            Ok(v) => Ok(v.into_value()),
133            Err(e) => Err(Error::HostFunction(e.to_string())),
134        }
135    }
136}
137
138/// Helper to wrap a Rust function as a host function.
139///
140/// This is used by the `host_fn!` macro but can also be used directly.
141pub fn wrap_host_fn<F, R>(f: F) -> impl Fn(&[Value], &ExecutionContext) -> Result<Value>
142where
143    F: Fn(&[Value], &ExecutionContext) -> R,
144    R: HostReturn,
145{
146    move |args: &[Value], ctx: &ExecutionContext| f(args, ctx).into_result()
147}
148
149/// Helper to create a host function with automatic argument extraction.
150///
151/// # Example
152///
153/// ```ignore
154/// let add = typed_host_fn(|a: i64, b: i64| -> i64 {
155///     a + b
156/// });
157/// ```
158pub fn typed_host_fn_0<F, R>(f: F) -> impl Fn(&[Value], &ExecutionContext) -> Result<Value>
159where
160    F: Fn() -> R,
161    R: HostReturn,
162{
163    move |_args: &[Value], _ctx: &ExecutionContext| f().into_result()
164}
165
166/// Typed host function with 1 argument.
167pub fn typed_host_fn_1<F, A, R>(f: F) -> impl Fn(&[Value], &ExecutionContext) -> Result<Value>
168where
169    F: Fn(A) -> R,
170    A: HostArg,
171    R: HostReturn,
172{
173    move |args: &[Value], _ctx: &ExecutionContext| {
174        let a = A::extract(args, 0)?;
175        f(a).into_result()
176    }
177}
178
179/// Typed host function with 2 arguments.
180pub fn typed_host_fn_2<F, A, B, R>(f: F) -> impl Fn(&[Value], &ExecutionContext) -> Result<Value>
181where
182    F: Fn(A, B) -> R,
183    A: HostArg,
184    B: HostArg,
185    R: HostReturn,
186{
187    move |args: &[Value], _ctx: &ExecutionContext| {
188        let a = A::extract(args, 0)?;
189        let b = B::extract(args, 1)?;
190        f(a, b).into_result()
191    }
192}
193
194/// Typed host function with 3 arguments.
195pub fn typed_host_fn_3<F, A, B, C, R>(
196    f: F,
197) -> impl Fn(&[Value], &ExecutionContext) -> Result<Value>
198where
199    F: Fn(A, B, C) -> R,
200    A: HostArg,
201    B: HostArg,
202    C: HostArg,
203    R: HostReturn,
204{
205    move |args: &[Value], _ctx: &ExecutionContext| {
206        let a = A::extract(args, 0)?;
207        let b = B::extract(args, 1)?;
208        let c = C::extract(args, 2)?;
209        f(a, b, c).into_result()
210    }
211}
212
213/// Typed host function with 4 arguments.
214pub fn typed_host_fn_4<F, A, B, C, D, R>(
215    f: F,
216) -> impl Fn(&[Value], &ExecutionContext) -> Result<Value>
217where
218    F: Fn(A, B, C, D) -> R,
219    A: HostArg,
220    B: HostArg,
221    C: HostArg,
222    D: HostArg,
223    R: HostReturn,
224{
225    move |args: &[Value], _ctx: &ExecutionContext| {
226        let a = A::extract(args, 0)?;
227        let b = B::extract(args, 1)?;
228        let c = C::extract(args, 2)?;
229        let d = D::extract(args, 3)?;
230        f(a, b, c, d).into_result()
231    }
232}
233
234/// Typed host function with context access and 0 arguments.
235pub fn typed_host_fn_ctx_0<F, R>(f: F) -> impl Fn(&[Value], &ExecutionContext) -> Result<Value>
236where
237    F: Fn(&ExecutionContext) -> R,
238    R: HostReturn,
239{
240    move |_args: &[Value], ctx: &ExecutionContext| f(ctx).into_result()
241}
242
243/// Typed host function with context access and 1 argument.
244pub fn typed_host_fn_ctx_1<F, A, R>(f: F) -> impl Fn(&[Value], &ExecutionContext) -> Result<Value>
245where
246    F: Fn(&ExecutionContext, A) -> R,
247    A: HostArg,
248    R: HostReturn,
249{
250    move |args: &[Value], ctx: &ExecutionContext| {
251        let a = A::extract(args, 0)?;
252        f(ctx, a).into_result()
253    }
254}
255
256/// Typed host function with context access and 2 arguments.
257pub fn typed_host_fn_ctx_2<F, A, B, R>(
258    f: F,
259) -> impl Fn(&[Value], &ExecutionContext) -> Result<Value>
260where
261    F: Fn(&ExecutionContext, A, B) -> R,
262    A: HostArg,
263    B: HostArg,
264    R: HostReturn,
265{
266    move |args: &[Value], ctx: &ExecutionContext| {
267        let a = A::extract(args, 0)?;
268        let b = B::extract(args, 1)?;
269        f(ctx, a, b).into_result()
270    }
271}
272
273/// Macro to define a typed host function.
274///
275/// # Examples
276///
277/// ```ignore
278/// // Simple function without context
279/// host_fn!(add(a: i64, b: i64) -> i64 {
280///     a + b
281/// });
282///
283/// // Function with context access
284/// host_fn!(ctx, log_message(msg: String) -> () {
285///     ctx.record_output(msg.len())?;
286///     println!("{}", msg);
287///     Ok(())
288/// });
289///
290/// // Function with optional argument
291/// host_fn!(greet(name: String, greeting: Optional<String>) -> String {
292///     let greeting = greeting.0.unwrap_or_else(|| "Hello".to_string());
293///     format!("{}, {}!", greeting, name)
294/// });
295/// ```
296#[macro_export]
297macro_rules! host_fn {
298    // No context, no args
299    ($name:ident() -> $ret:ty $body:block) => {
300        $crate::macros::typed_host_fn_0(|| -> $ret $body)
301    };
302
303    // No context, 1 arg
304    ($name:ident($a:ident: $at:ty) -> $ret:ty $body:block) => {
305        $crate::macros::typed_host_fn_1(|$a: $at| -> $ret $body)
306    };
307
308    // No context, 2 args
309    ($name:ident($a:ident: $at:ty, $b:ident: $bt:ty) -> $ret:ty $body:block) => {
310        $crate::macros::typed_host_fn_2(|$a: $at, $b: $bt| -> $ret $body)
311    };
312
313    // No context, 3 args
314    ($name:ident($a:ident: $at:ty, $b:ident: $bt:ty, $c:ident: $ct:ty) -> $ret:ty $body:block) => {
315        $crate::macros::typed_host_fn_3(|$a: $at, $b: $bt, $c: $ct| -> $ret $body)
316    };
317
318    // No context, 4 args
319    ($name:ident($a:ident: $at:ty, $b:ident: $bt:ty, $c:ident: $ct:ty, $d:ident: $dt:ty) -> $ret:ty $body:block) => {
320        $crate::macros::typed_host_fn_4(|$a: $at, $b: $bt, $c: $ct, $d: $dt| -> $ret $body)
321    };
322
323    // With context, no args
324    (ctx, $name:ident() -> $ret:ty $body:block) => {
325        $crate::macros::typed_host_fn_ctx_0(|ctx: &$crate::ExecutionContext| -> $ret $body)
326    };
327
328    // With context, 1 arg
329    (ctx, $name:ident($a:ident: $at:ty) -> $ret:ty $body:block) => {
330        $crate::macros::typed_host_fn_ctx_1(|ctx: &$crate::ExecutionContext, $a: $at| -> $ret $body)
331    };
332
333    // With context, 2 args
334    (ctx, $name:ident($a:ident: $at:ty, $b:ident: $bt:ty) -> $ret:ty $body:block) => {
335        $crate::macros::typed_host_fn_ctx_2(|ctx: &$crate::ExecutionContext, $a: $at, $b: $bt| -> $ret $body)
336    };
337}
338
339/// Builder for creating host function registries with typed functions.
340pub struct HostFnBuilder {
341    registry: crate::engine::HostRegistry,
342}
343
344impl HostFnBuilder {
345    /// Create a new builder.
346    pub fn new() -> Self {
347        Self {
348            registry: crate::engine::HostRegistry::new(),
349        }
350    }
351
352    /// Register a global function.
353    pub fn register<S, F>(mut self, name: S, f: F) -> Self
354    where
355        S: Into<String>,
356        F: Fn(&[Value], &ExecutionContext) -> Result<Value> + Send + Sync + 'static,
357    {
358        self.registry.register(name, f);
359        self
360    }
361
362    /// Register a module function.
363    pub fn register_module<M, N, F>(mut self, module: M, name: N, f: F) -> Self
364    where
365        M: Into<String>,
366        N: Into<String>,
367        F: Fn(&[Value], &ExecutionContext) -> Result<Value> + Send + Sync + 'static,
368    {
369        self.registry.register_module(module, name, f);
370        self
371    }
372
373    /// Build the registry.
374    pub fn build(self) -> crate::engine::HostRegistry {
375        self.registry
376    }
377}
378
379impl Default for HostFnBuilder {
380    fn default() -> Self {
381        Self::new()
382    }
383}
384
385#[cfg(test)]
386mod tests {
387    use super::*;
388
389    #[test]
390    fn test_arg_extraction() {
391        let args = vec![Value::Int(42), Value::String("hello".into())];
392
393        let a: i64 = i64::extract(&args, 0).unwrap();
394        assert_eq!(a, 42);
395
396        let b: String = String::extract(&args, 1).unwrap();
397        assert_eq!(b, "hello");
398    }
399
400    #[test]
401    fn test_arg_extraction_missing() {
402        let args = vec![Value::Int(42)];
403        let result: std::result::Result<i64, _> = i64::extract(&args, 5);
404        assert!(result.is_err());
405    }
406
407    #[test]
408    fn test_optional_arg() {
409        let args = vec![Value::Int(42)];
410
411        let opt: Optional<i64> = Optional::extract(&args, 0).unwrap();
412        assert_eq!(opt.0, Some(42));
413
414        let opt: Optional<i64> = Optional::extract(&args, 1).unwrap();
415        assert_eq!(opt.0, None);
416
417        let args_with_null = vec![Value::Null];
418        let opt: Optional<i64> = Optional::extract(&args_with_null, 0).unwrap();
419        assert_eq!(opt.0, None);
420    }
421
422    #[test]
423    fn test_rest_args() {
424        let args = vec![
425            Value::Int(1),
426            Value::Int(2),
427            Value::Int(3),
428            Value::Int(4),
429        ];
430
431        let rest: Rest<i64> = Rest::extract(&args, 1).unwrap();
432        assert_eq!(rest.0, vec![2, 3, 4]);
433    }
434
435    #[test]
436    fn test_typed_host_fn() {
437        use crate::sandbox::{Sandbox, SandboxConfig};
438        use crate::Capabilities;
439        use crate::Limits;
440
441        let add = typed_host_fn_2(|a: i64, b: i64| -> i64 { a + b });
442
443        let sandbox = Sandbox::new(SandboxConfig::default()).unwrap();
444        let ctx = ExecutionContext::new(1, Capabilities::none(), Limits::default(), sandbox);
445
446        let result = add(&[Value::Int(3), Value::Int(4)], &ctx).unwrap();
447        assert_eq!(result, Value::Int(7));
448    }
449
450    #[test]
451    fn test_typed_host_fn_with_result() {
452        use crate::sandbox::{Sandbox, SandboxConfig};
453        use crate::Capabilities;
454        use crate::Limits;
455
456        let div = typed_host_fn_2(
457            |a: i64, b: i64| -> std::result::Result<i64, &'static str> {
458                if b == 0 {
459                    Err("division by zero")
460                } else {
461                    Ok(a / b)
462                }
463            },
464        );
465
466        let sandbox = Sandbox::new(SandboxConfig::default()).unwrap();
467        let ctx = ExecutionContext::new(1, Capabilities::none(), Limits::default(), sandbox);
468
469        let result = div(&[Value::Int(10), Value::Int(2)], &ctx).unwrap();
470        assert_eq!(result, Value::Int(5));
471
472        let result = div(&[Value::Int(10), Value::Int(0)], &ctx);
473        assert!(result.is_err());
474    }
475
476    #[test]
477    fn test_host_fn_builder() {
478        let registry = HostFnBuilder::new()
479            .register("test", |_args, _ctx| Ok(Value::Int(42)))
480            .register_module("math", "pi", |_args, _ctx| Ok(Value::Float(3.14159)))
481            .build();
482
483        assert!(registry.get("test").is_some());
484        assert!(registry.get_module("math", "pi").is_some());
485    }
486}