Skip to main content

harn_vm/vm/
dispatch.rs

1use std::future::Future;
2use std::pin::Pin;
3use std::rc::Rc;
4
5use crate::chunk::CompiledFunction;
6use crate::value::{ErrorCategory, VmClosure, VmError, VmValue};
7
8use super::async_builtin::CURRENT_ASYNC_BUILTIN_CHILD_VM;
9use super::{ScopeSpan, Vm};
10
11impl Vm {
12    /// Register a sync builtin function.
13    pub fn register_builtin<F>(&mut self, name: &str, f: F)
14    where
15        F: Fn(&[VmValue], &mut String) -> Result<VmValue, VmError> + 'static,
16    {
17        self.builtins.insert(name.to_string(), Rc::new(f));
18    }
19
20    /// Remove a sync builtin (so an async version can take precedence).
21    pub fn unregister_builtin(&mut self, name: &str) {
22        self.builtins.remove(name);
23    }
24
25    /// Register an async builtin function.
26    pub fn register_async_builtin<F, Fut>(&mut self, name: &str, f: F)
27    where
28        F: Fn(Vec<VmValue>) -> Fut + 'static,
29        Fut: Future<Output = Result<VmValue, VmError>> + 'static,
30    {
31        self.async_builtins
32            .insert(name.to_string(), Rc::new(move |args| Box::pin(f(args))));
33    }
34
35    /// Call a closure (used by method calls like .map/.filter etc.)
36    /// Uses recursive execution for simplicity in method dispatch.
37    pub(crate) fn call_closure<'a>(
38        &'a mut self,
39        closure: &'a VmClosure,
40        args: &'a [VmValue],
41        _parent_functions: &'a [CompiledFunction],
42    ) -> Pin<Box<dyn Future<Output = Result<VmValue, VmError>> + 'a>> {
43        Box::pin(async move {
44            let saved_env = self.env.clone();
45            let saved_frames = std::mem::take(&mut self.frames);
46            let saved_handlers = std::mem::take(&mut self.exception_handlers);
47            let saved_iterators = std::mem::take(&mut self.iterators);
48            let saved_deadlines = std::mem::take(&mut self.deadlines);
49
50            let mut call_env = Self::closure_call_env(&saved_env, closure);
51            call_env.push_scope();
52
53            let default_start = closure
54                .func
55                .default_start
56                .unwrap_or(closure.func.params.len());
57            let param_count = closure.func.params.len();
58            for (i, param) in closure.func.params.iter().enumerate() {
59                if closure.func.has_rest_param && i == param_count - 1 {
60                    let rest_args = if i < args.len() {
61                        args[i..].to_vec()
62                    } else {
63                        Vec::new()
64                    };
65                    let _ =
66                        call_env.define(param, VmValue::List(std::rc::Rc::new(rest_args)), false);
67                } else if i < args.len() {
68                    let _ = call_env.define(param, args[i].clone(), false);
69                } else if i < default_start {
70                    let _ = call_env.define(param, VmValue::Nil, false);
71                }
72            }
73
74            self.env = call_env;
75            let argc = args.len();
76            let saved_source_dir = if let Some(ref dir) = closure.source_dir {
77                let prev = crate::stdlib::process::VM_SOURCE_DIR.with(|sd| sd.borrow().clone());
78                crate::stdlib::set_thread_source_dir(dir);
79                prev
80            } else {
81                None
82            };
83            let result = self
84                .run_chunk_entry(
85                    &closure.func.chunk,
86                    argc,
87                    saved_source_dir,
88                    closure.module_functions.clone(),
89                    closure.module_state.clone(),
90                )
91                .await;
92
93            self.env = saved_env;
94            self.frames = saved_frames;
95            self.exception_handlers = saved_handlers;
96            self.iterators = saved_iterators;
97            self.deadlines = saved_deadlines;
98
99            result
100        })
101    }
102
103    /// Invoke a value as a callable. Supports `VmValue::Closure` and
104    /// `VmValue::BuiltinRef`, so builtin names passed by reference (e.g.
105    /// `dict.rekey(snake_to_camel)`) dispatch through the same code path as
106    /// user-defined closures.
107    #[allow(clippy::manual_async_fn)]
108    pub(crate) fn call_callable_value<'a>(
109        &'a mut self,
110        callable: &'a VmValue,
111        args: &'a [VmValue],
112        functions: &'a [CompiledFunction],
113    ) -> Pin<Box<dyn Future<Output = Result<VmValue, VmError>> + 'a>> {
114        Box::pin(async move {
115            match callable {
116                VmValue::Closure(closure) => self.call_closure(closure, args, functions).await,
117                VmValue::BuiltinRef(name) => {
118                    let name_owned = name.to_string();
119                    self.call_named_builtin(&name_owned, args.to_vec()).await
120                }
121                other => Err(VmError::TypeError(format!(
122                    "expected callable, got {}",
123                    other.type_name()
124                ))),
125            }
126        })
127    }
128
129    /// Returns true if `v` is callable via `call_callable_value`.
130    pub(crate) fn is_callable_value(v: &VmValue) -> bool {
131        matches!(v, VmValue::Closure(_) | VmValue::BuiltinRef(_))
132    }
133
134    /// Public wrapper for `call_closure`, used by the MCP server to invoke
135    /// tool handler closures from outside the VM execution loop.
136    pub async fn call_closure_pub(
137        &mut self,
138        closure: &VmClosure,
139        args: &[VmValue],
140        functions: &[CompiledFunction],
141    ) -> Result<VmValue, VmError> {
142        self.call_closure(closure, args, functions).await
143    }
144
145    /// Resolve a named builtin: sync builtins → async builtins → bridge → error.
146    /// Used by Call, TailCall, and Pipe handlers to avoid duplicating this lookup.
147    pub(crate) async fn call_named_builtin(
148        &mut self,
149        name: &str,
150        args: Vec<VmValue>,
151    ) -> Result<VmValue, VmError> {
152        // Auto-trace LLM calls and tool calls.
153        let span_kind = match name {
154            "llm_call" | "llm_stream" | "agent_loop" => Some(crate::tracing::SpanKind::LlmCall),
155            "mcp_call" => Some(crate::tracing::SpanKind::ToolCall),
156            _ => None,
157        };
158        let _span = span_kind.map(|kind| ScopeSpan::new(kind, name.to_string()));
159
160        // Sandbox check: deny builtins blocked by --deny/--allow flags.
161        if self.denied_builtins.contains(name) {
162            return Err(VmError::CategorizedError {
163                message: format!("Tool '{}' is not permitted.", name),
164                category: ErrorCategory::ToolRejected,
165            });
166        }
167        crate::orchestration::enforce_current_policy_for_builtin(name, &args)?;
168        if let Some(builtin) = self.builtins.get(name).cloned() {
169            builtin(&args, &mut self.output)
170        } else if let Some(async_builtin) = self.async_builtins.get(name).cloned() {
171            CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
172                slot.borrow_mut().push(self.child_vm());
173            });
174            let result = async_builtin(args).await;
175            CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
176                slot.borrow_mut().pop();
177            });
178            result
179        } else if let Some(bridge) = &self.bridge {
180            crate::orchestration::enforce_current_policy_for_bridge_builtin(name)?;
181            let args_json: Vec<serde_json::Value> =
182                args.iter().map(crate::llm::vm_value_to_json).collect();
183            let result = bridge
184                .call(
185                    "builtin_call",
186                    serde_json::json!({"name": name, "args": args_json}),
187                )
188                .await?;
189            Ok(crate::bridge::json_result_to_vm_value(&result))
190        } else {
191            let all_builtins = self
192                .builtins
193                .keys()
194                .chain(self.async_builtins.keys())
195                .map(|s| s.as_str());
196            if let Some(suggestion) = crate::value::closest_match(name, all_builtins) {
197                return Err(VmError::Runtime(format!(
198                    "Undefined builtin: {name} (did you mean `{suggestion}`?)"
199                )));
200            }
201            Err(VmError::UndefinedBuiltin(name.to_string()))
202        }
203    }
204}