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" | "agent_loop" => Some(crate::tracing::SpanKind::LlmCall),
178            "mcp_call" => Some(crate::tracing::SpanKind::ToolCall),
179            _ => None,
180        };
181        let _span = span_kind.map(|kind| ScopeSpan::new(kind, name.to_string()));
182
183        if self.denied_builtins.contains(name) {
184            return Some(Err(VmError::CategorizedError {
185                message: format!("Tool '{}' is not permitted.", name),
186                category: ErrorCategory::ToolRejected,
187            }));
188        }
189        if let Err(err) = crate::orchestration::enforce_current_policy_for_builtin(name, args) {
190            return Some(Err(err));
191        }
192
193        Some(builtin(args, &mut self.output))
194    }
195
196    /// Returns true if `v` is callable via `call_callable_value`.
197    pub(crate) fn is_callable_value(v: &VmValue) -> bool {
198        matches!(
199            v,
200            VmValue::Closure(_) | VmValue::BuiltinRef(_) | VmValue::BuiltinRefId { .. }
201        )
202    }
203
204    /// Public wrapper for `call_closure`, used by the MCP server to invoke
205    /// tool handler closures from outside the VM execution loop.
206    pub async fn call_closure_pub(
207        &mut self,
208        closure: &VmClosure,
209        args: &[VmValue],
210    ) -> Result<VmValue, VmError> {
211        self.call_closure(closure, args).await
212    }
213
214    /// Resolve a named builtin: sync builtins → async builtins → bridge → error.
215    /// Used by Call, TailCall, and Pipe handlers to avoid duplicating this lookup.
216    pub(crate) async fn call_named_builtin(
217        &mut self,
218        name: &str,
219        args: Vec<VmValue>,
220    ) -> Result<VmValue, VmError> {
221        self.call_builtin_impl(name, args, None).await
222    }
223
224    pub(crate) async fn call_builtin_id_or_name(
225        &mut self,
226        id: BuiltinId,
227        name: &str,
228        args: Vec<VmValue>,
229    ) -> Result<VmValue, VmError> {
230        self.call_builtin_impl(name, args, Some(id)).await
231    }
232
233    async fn call_builtin_impl(
234        &mut self,
235        name: &str,
236        args: Vec<VmValue>,
237        direct_id: Option<BuiltinId>,
238    ) -> Result<VmValue, VmError> {
239        // Auto-trace LLM calls and tool calls.
240        let span_kind = match name {
241            "llm_call" | "llm_stream" | "agent_loop" => Some(crate::tracing::SpanKind::LlmCall),
242            "mcp_call" => Some(crate::tracing::SpanKind::ToolCall),
243            _ => None,
244        };
245        let _span = span_kind.map(|kind| ScopeSpan::new(kind, name.to_string()));
246
247        // Sandbox check: deny builtins blocked by --deny/--allow flags.
248        if self.denied_builtins.contains(name) {
249            return Err(VmError::CategorizedError {
250                message: format!("Tool '{}' is not permitted.", name),
251                category: ErrorCategory::ToolRejected,
252            });
253        }
254        crate::orchestration::enforce_current_policy_for_builtin(name, &args)?;
255
256        if let Some(result) =
257            crate::runtime_context::dispatch_runtime_context_builtin(self, name, &args)
258        {
259            return result;
260        }
261
262        if let Some(id) = direct_id {
263            if let Some(entry) = self.builtins_by_id.get(&id).cloned() {
264                if entry.name.as_ref() == name {
265                    return self.call_builtin_entry(entry.dispatch, args).await;
266                }
267            }
268        }
269
270        if let Some(builtin) = self.builtins.get(name).cloned() {
271            self.call_builtin_entry(VmBuiltinDispatch::Sync(builtin), args)
272                .await
273        } else if let Some(async_builtin) = self.async_builtins.get(name).cloned() {
274            self.call_builtin_entry(VmBuiltinDispatch::Async(async_builtin), args)
275                .await
276        } else if let Some(bridge) = &self.bridge {
277            crate::orchestration::enforce_current_policy_for_bridge_builtin(name)?;
278            let args_json: Vec<serde_json::Value> =
279                args.iter().map(crate::llm::vm_value_to_json).collect();
280            let result = bridge
281                .call(
282                    "builtin_call",
283                    serde_json::json!({"name": name, "args": args_json}),
284                )
285                .await?;
286            Ok(crate::bridge::json_result_to_vm_value(&result))
287        } else {
288            let all_builtins = self
289                .builtins
290                .keys()
291                .chain(self.async_builtins.keys())
292                .map(|s| s.as_str());
293            if let Some(suggestion) = crate::value::closest_match(name, all_builtins) {
294                return Err(VmError::Runtime(format!(
295                    "Undefined builtin: {name} (did you mean `{suggestion}`?)"
296                )));
297            }
298            Err(VmError::UndefinedBuiltin(name.to_string()))
299        }
300    }
301
302    async fn call_builtin_entry(
303        &mut self,
304        dispatch: VmBuiltinDispatch,
305        args: Vec<VmValue>,
306    ) -> Result<VmValue, VmError> {
307        match dispatch {
308            VmBuiltinDispatch::Sync(builtin) => builtin(&args, &mut self.output),
309            VmBuiltinDispatch::Async(async_builtin) => {
310                CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
311                    slot.borrow_mut().push(self.child_vm());
312                });
313                let result = async_builtin(args).await;
314                CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
315                    slot.borrow_mut().pop();
316                });
317                result
318            }
319        }
320    }
321}