Skip to main content

harn_vm/vm/
dispatch.rs

1use std::future::Future;
2use std::pin::Pin;
3use std::rc::Rc;
4
5use crate::value::{ErrorCategory, VmClosure, VmError, VmValue};
6use crate::BuiltinId;
7
8use super::async_builtin::CURRENT_ASYNC_BUILTIN_CHILD_VM;
9use super::{ScopeSpan, Vm, VmBuiltinDispatch, VmBuiltinEntry};
10
11impl Vm {
12    fn index_builtin_id(&mut self, name: &str, dispatch: VmBuiltinDispatch) {
13        let id = BuiltinId::from_name(name);
14        if self.builtin_id_collisions.contains(&id) {
15            return;
16        }
17        if let Some(existing) = self.builtins_by_id.get(&id) {
18            if existing.name.as_ref() != name {
19                self.builtins_by_id.remove(&id);
20                self.builtin_id_collisions.insert(id);
21                return;
22            }
23        }
24        self.builtins_by_id.insert(
25            id,
26            VmBuiltinEntry {
27                name: Rc::from(name),
28                dispatch,
29            },
30        );
31    }
32
33    fn refresh_builtin_id(&mut self, name: &str) {
34        if let Some(builtin) = self.builtins.get(name).cloned() {
35            self.index_builtin_id(name, VmBuiltinDispatch::Sync(builtin));
36        } else if let Some(async_builtin) = self.async_builtins.get(name).cloned() {
37            self.index_builtin_id(name, VmBuiltinDispatch::Async(async_builtin));
38        } else {
39            let id = BuiltinId::from_name(name);
40            if self
41                .builtins_by_id
42                .get(&id)
43                .is_some_and(|entry| entry.name.as_ref() == name)
44            {
45                self.builtins_by_id.remove(&id);
46            }
47        }
48    }
49
50    /// Register a sync builtin function.
51    pub fn register_builtin<F>(&mut self, name: &str, f: F)
52    where
53        F: Fn(&[VmValue], &mut String) -> Result<VmValue, VmError> + 'static,
54    {
55        self.builtins.insert(name.to_string(), Rc::new(f));
56        self.refresh_builtin_id(name);
57    }
58
59    /// Remove a sync builtin (so an async version can take precedence).
60    pub fn unregister_builtin(&mut self, name: &str) {
61        self.builtins.remove(name);
62        self.refresh_builtin_id(name);
63    }
64
65    /// Register an async builtin function.
66    pub fn register_async_builtin<F, Fut>(&mut self, name: &str, f: F)
67    where
68        F: Fn(Vec<VmValue>) -> Fut + 'static,
69        Fut: Future<Output = Result<VmValue, VmError>> + 'static,
70    {
71        self.async_builtins
72            .insert(name.to_string(), Rc::new(move |args| Box::pin(f(args))));
73        self.refresh_builtin_id(name);
74    }
75
76    pub(crate) fn registered_builtin_id(&self, name: &str) -> Option<BuiltinId> {
77        let id = BuiltinId::from_name(name);
78        if self
79            .builtins_by_id
80            .get(&id)
81            .is_some_and(|entry| entry.name.as_ref() == name)
82        {
83            Some(id)
84        } else {
85            None
86        }
87    }
88
89    /// Call a closure (used by method calls like .map/.filter etc.)
90    /// Uses recursive execution for simplicity in method dispatch.
91    pub(crate) fn call_closure<'a>(
92        &'a mut self,
93        closure: &'a VmClosure,
94        args: &'a [VmValue],
95    ) -> Pin<Box<dyn Future<Output = Result<VmValue, VmError>> + 'a>> {
96        Box::pin(async move {
97            let saved_env = self.env.clone();
98            let mut call_env = self.closure_call_env_for_current_frame(closure);
99            let saved_frames = std::mem::take(&mut self.frames);
100            let saved_handlers = std::mem::take(&mut self.exception_handlers);
101            let saved_iterators = std::mem::take(&mut self.iterators);
102            let saved_deadlines = std::mem::take(&mut self.deadlines);
103
104            call_env.push_scope();
105
106            self.env = call_env;
107            let argc = args.len();
108            let mut local_slots = Self::fresh_local_slots(&closure.func.chunk);
109            Self::bind_param_slots(&mut local_slots, &closure.func, args, false);
110            let saved_source_dir = if let Some(ref dir) = closure.source_dir {
111                let prev = crate::stdlib::process::VM_SOURCE_DIR.with(|sd| sd.borrow().clone());
112                crate::stdlib::set_thread_source_dir(dir);
113                prev
114            } else {
115                None
116            };
117            let result = self
118                .run_chunk_ref(
119                    Rc::clone(&closure.func.chunk),
120                    argc,
121                    saved_source_dir,
122                    closure.module_functions.clone(),
123                    closure.module_state.clone(),
124                    Some(local_slots),
125                )
126                .await;
127
128            self.env = saved_env;
129            self.frames = saved_frames;
130            self.exception_handlers = saved_handlers;
131            self.iterators = saved_iterators;
132            self.deadlines = saved_deadlines;
133
134            result
135        })
136    }
137
138    /// Invoke a value as a callable. Supports `VmValue::Closure` and
139    /// `VmValue::BuiltinRef`, so builtin names passed by reference (e.g.
140    /// `dict.rekey(snake_to_camel)`) dispatch through the same code path as
141    /// user-defined closures.
142    #[allow(clippy::manual_async_fn)]
143    pub(crate) fn call_callable_value<'a>(
144        &'a mut self,
145        callable: &'a VmValue,
146        args: &'a [VmValue],
147    ) -> Pin<Box<dyn Future<Output = Result<VmValue, VmError>> + 'a>> {
148        Box::pin(async move {
149            match callable {
150                VmValue::Closure(closure) => self.call_closure(closure, args).await,
151                VmValue::BuiltinRef(name) => {
152                    if let Some(result) = self.call_sync_builtin_by_ref(name, args) {
153                        result
154                    } else {
155                        self.call_named_builtin(name, args.to_vec()).await
156                    }
157                }
158                VmValue::BuiltinRefId { id, name } => {
159                    self.call_builtin_id_or_name(*id, name, args.to_vec()).await
160                }
161                other => Err(VmError::TypeError(format!(
162                    "expected callable, got {}",
163                    other.type_name()
164                ))),
165            }
166        })
167    }
168
169    fn call_sync_builtin_by_ref(
170        &mut self,
171        name: &str,
172        args: &[VmValue],
173    ) -> Option<Result<VmValue, VmError>> {
174        let builtin = self.builtins.get(name).cloned()?;
175
176        let span_kind = match name {
177            "llm_call" | "llm_stream" | "llm_stream_call" | "agent_loop" => {
178                Some(crate::tracing::SpanKind::LlmCall)
179            }
180            "mcp_call" => Some(crate::tracing::SpanKind::ToolCall),
181            _ => None,
182        };
183        let _span = span_kind.map(|kind| ScopeSpan::new(kind, name.to_string()));
184
185        if self.denied_builtins.contains(name) {
186            return Some(Err(VmError::CategorizedError {
187                message: format!("Tool '{}' is not permitted.", name),
188                category: ErrorCategory::ToolRejected,
189            }));
190        }
191        if let Err(err) = crate::orchestration::enforce_current_policy_for_builtin(name, args) {
192            return Some(Err(err));
193        }
194
195        Some(builtin(args, &mut self.output))
196    }
197
198    /// Returns true if `v` is callable via `call_callable_value`.
199    pub(crate) fn is_callable_value(v: &VmValue) -> bool {
200        matches!(
201            v,
202            VmValue::Closure(_) | VmValue::BuiltinRef(_) | VmValue::BuiltinRefId { .. }
203        )
204    }
205
206    /// Public wrapper for `call_closure`, used by the MCP server to invoke
207    /// tool handler closures from outside the VM execution loop.
208    pub async fn call_closure_pub(
209        &mut self,
210        closure: &VmClosure,
211        args: &[VmValue],
212    ) -> Result<VmValue, VmError> {
213        self.cancel_grace_instructions_remaining = None;
214        self.call_closure(closure, args).await
215    }
216
217    /// Resolve a named builtin: sync builtins → async builtins → bridge → error.
218    /// Used by Call, TailCall, and Pipe handlers to avoid duplicating this lookup.
219    pub(crate) async fn call_named_builtin(
220        &mut self,
221        name: &str,
222        args: Vec<VmValue>,
223    ) -> Result<VmValue, VmError> {
224        self.call_builtin_impl(name, args, None).await
225    }
226
227    pub(crate) async fn call_builtin_id_or_name(
228        &mut self,
229        id: BuiltinId,
230        name: &str,
231        args: Vec<VmValue>,
232    ) -> Result<VmValue, VmError> {
233        self.call_builtin_impl(name, args, Some(id)).await
234    }
235
236    async fn call_builtin_impl(
237        &mut self,
238        name: &str,
239        args: Vec<VmValue>,
240        direct_id: Option<BuiltinId>,
241    ) -> Result<VmValue, VmError> {
242        // Auto-trace LLM calls and tool calls.
243        let span_kind = match name {
244            "llm_call" | "llm_stream" | "llm_stream_call" | "agent_loop" => {
245                Some(crate::tracing::SpanKind::LlmCall)
246            }
247            "mcp_call" => Some(crate::tracing::SpanKind::ToolCall),
248            _ => None,
249        };
250        let _span = span_kind.map(|kind| ScopeSpan::new(kind, name.to_string()));
251
252        // Sandbox check: deny builtins blocked by --deny/--allow flags.
253        if self.denied_builtins.contains(name) {
254            return Err(VmError::CategorizedError {
255                message: format!("Tool '{}' is not permitted.", name),
256                category: ErrorCategory::ToolRejected,
257            });
258        }
259        crate::orchestration::enforce_current_policy_for_builtin(name, &args)?;
260
261        if let Some(result) =
262            crate::runtime_context::dispatch_runtime_context_builtin(self, name, &args)
263        {
264            return result;
265        }
266
267        if let Some(id) = direct_id {
268            if let Some(entry) = self.builtins_by_id.get(&id).cloned() {
269                if entry.name.as_ref() == name {
270                    return self.call_builtin_entry(entry.dispatch, args).await;
271                }
272            }
273        }
274
275        if let Some(builtin) = self.builtins.get(name).cloned() {
276            self.call_builtin_entry(VmBuiltinDispatch::Sync(builtin), args)
277                .await
278        } else if let Some(async_builtin) = self.async_builtins.get(name).cloned() {
279            self.call_builtin_entry(VmBuiltinDispatch::Async(async_builtin), args)
280                .await
281        } else if let Some(bridge) = &self.bridge {
282            crate::orchestration::enforce_current_policy_for_bridge_builtin(name)?;
283            let args_json: Vec<serde_json::Value> =
284                args.iter().map(crate::llm::vm_value_to_json).collect();
285            let result = bridge
286                .call(
287                    "builtin_call",
288                    serde_json::json!({"name": name, "args": args_json}),
289                )
290                .await?;
291            Ok(crate::bridge::json_result_to_vm_value(&result))
292        } else {
293            let all_builtins = self
294                .builtins
295                .keys()
296                .chain(self.async_builtins.keys())
297                .map(|s| s.as_str());
298            if let Some(suggestion) = crate::value::closest_match(name, all_builtins) {
299                return Err(VmError::Runtime(format!(
300                    "Undefined builtin: {name} (did you mean `{suggestion}`?)"
301                )));
302            }
303            Err(VmError::UndefinedBuiltin(name.to_string()))
304        }
305    }
306
307    async fn call_builtin_entry(
308        &mut self,
309        dispatch: VmBuiltinDispatch,
310        args: Vec<VmValue>,
311    ) -> Result<VmValue, VmError> {
312        match dispatch {
313            VmBuiltinDispatch::Sync(builtin) => builtin(&args, &mut self.output),
314            VmBuiltinDispatch::Async(async_builtin) => {
315                CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
316                    slot.borrow_mut().push(self.child_vm());
317                });
318                let result = async_builtin(args).await;
319                CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
320                    slot.borrow_mut().pop();
321                });
322                result
323            }
324        }
325    }
326}