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, VmBuiltinKind, VmBuiltinMetadata};
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                Rc::make_mut(&mut self.builtins_by_id).remove(&id);
20                Rc::make_mut(&mut self.builtin_id_collisions).insert(id);
21                return;
22            }
23        }
24        Rc::make_mut(&mut 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                Rc::make_mut(&mut 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        Rc::make_mut(&mut self.builtins).insert(name.to_string(), Rc::new(f));
56        Rc::make_mut(&mut self.builtin_metadata)
57            .insert(name.to_string(), VmBuiltinMetadata::sync(name.to_string()));
58        self.refresh_builtin_id(name);
59    }
60
61    /// Register a sync builtin function with discoverable metadata.
62    pub fn register_builtin_with_metadata<F>(&mut self, metadata: VmBuiltinMetadata, f: F)
63    where
64        F: Fn(&[VmValue], &mut String) -> Result<VmValue, VmError> + 'static,
65    {
66        let name = metadata.name().to_string();
67        Rc::make_mut(&mut self.builtins).insert(name.clone(), Rc::new(f));
68        Rc::make_mut(&mut self.builtin_metadata)
69            .insert(name.clone(), metadata.with_kind(VmBuiltinKind::Sync));
70        self.refresh_builtin_id(&name);
71    }
72
73    /// Remove a sync builtin (so an async version can take precedence).
74    pub fn unregister_builtin(&mut self, name: &str) {
75        Rc::make_mut(&mut self.builtins).remove(name);
76        if self.async_builtins.contains_key(name) {
77            Rc::make_mut(&mut self.builtin_metadata).insert(
78                name.to_string(),
79                VmBuiltinMetadata::async_builtin(name.to_string()),
80            );
81        } else {
82            Rc::make_mut(&mut self.builtin_metadata).remove(name);
83        }
84        self.refresh_builtin_id(name);
85    }
86
87    /// Register an async builtin function.
88    pub fn register_async_builtin<F, Fut>(&mut self, name: &str, f: F)
89    where
90        F: Fn(Vec<VmValue>) -> Fut + 'static,
91        Fut: Future<Output = Result<VmValue, VmError>> + 'static,
92    {
93        Rc::make_mut(&mut self.async_builtins)
94            .insert(name.to_string(), Rc::new(move |args| Box::pin(f(args))));
95        Rc::make_mut(&mut self.builtin_metadata).insert(
96            name.to_string(),
97            VmBuiltinMetadata::async_builtin(name.to_string()),
98        );
99        self.refresh_builtin_id(name);
100    }
101
102    /// Register an async builtin function with discoverable metadata.
103    pub fn register_async_builtin_with_metadata<F, Fut>(
104        &mut self,
105        metadata: VmBuiltinMetadata,
106        f: F,
107    ) where
108        F: Fn(Vec<VmValue>) -> Fut + 'static,
109        Fut: Future<Output = Result<VmValue, VmError>> + 'static,
110    {
111        let name = metadata.name().to_string();
112        Rc::make_mut(&mut self.async_builtins)
113            .insert(name.clone(), Rc::new(move |args| Box::pin(f(args))));
114        Rc::make_mut(&mut self.builtin_metadata)
115            .insert(name.clone(), metadata.with_kind(VmBuiltinKind::Async));
116        self.refresh_builtin_id(&name);
117    }
118
119    pub(crate) fn registered_builtin_id(&self, name: &str) -> Option<BuiltinId> {
120        let id = BuiltinId::from_name(name);
121        if self
122            .builtins_by_id
123            .get(&id)
124            .is_some_and(|entry| entry.name.as_ref() == name)
125        {
126            Some(id)
127        } else {
128            None
129        }
130    }
131
132    /// Call a closure (used by method calls like .map/.filter etc.)
133    /// Uses recursive execution for simplicity in method dispatch.
134    pub(crate) fn call_closure<'a>(
135        &'a mut self,
136        closure: &'a VmClosure,
137        args: &'a [VmValue],
138    ) -> Pin<Box<dyn Future<Output = Result<VmValue, VmError>> + 'a>> {
139        Box::pin(async move {
140            crate::typecheck::validate_user_call(&closure.func, args, None)?;
141            let saved_env = self.env.clone();
142            let mut call_env = self.closure_call_env_for_current_frame(closure);
143            let saved_frames = std::mem::take(&mut self.frames);
144            let saved_handlers = std::mem::take(&mut self.exception_handlers);
145            let saved_iterators = std::mem::take(&mut self.iterators);
146            let saved_deadlines = std::mem::take(&mut self.deadlines);
147            let active_context = (!crate::step_runtime::is_tracked_function(&closure.func.name))
148                .then(crate::step_runtime::take_active_context);
149
150            call_env.push_scope();
151
152            self.env = call_env;
153            let argc = args.len();
154            let mut local_slots = Self::fresh_local_slots(&closure.func.chunk);
155            Self::bind_param_slots(&mut local_slots, &closure.func, args, false);
156            let saved_source_dir = if let Some(ref dir) = closure.source_dir {
157                let prev = crate::stdlib::process::VM_SOURCE_DIR.with(|sd| sd.borrow().clone());
158                crate::stdlib::set_thread_source_dir(dir);
159                prev
160            } else {
161                None
162            };
163            let result = self
164                .run_chunk_ref(
165                    Rc::clone(&closure.func.chunk),
166                    argc,
167                    saved_source_dir,
168                    closure.module_functions.clone(),
169                    closure.module_state.clone(),
170                    Some(local_slots),
171                )
172                .await;
173
174            self.env = saved_env;
175            self.frames = saved_frames;
176            self.exception_handlers = saved_handlers;
177            self.iterators = saved_iterators;
178            self.deadlines = saved_deadlines;
179            if let Some(active_context) = active_context {
180                crate::step_runtime::restore_active_context(active_context);
181            }
182
183            result
184        })
185    }
186
187    /// Invoke a value as a callable. Supports `VmValue::Closure` and
188    /// `VmValue::BuiltinRef`, so builtin names passed by reference (e.g.
189    /// `dict.rekey(snake_to_camel)`) dispatch through the same code path as
190    /// user-defined closures.
191    #[allow(clippy::manual_async_fn)]
192    pub(crate) fn call_callable_value<'a>(
193        &'a mut self,
194        callable: &'a VmValue,
195        args: &'a [VmValue],
196    ) -> Pin<Box<dyn Future<Output = Result<VmValue, VmError>> + 'a>> {
197        Box::pin(async move {
198            match callable {
199                VmValue::Closure(closure) => self.call_closure(closure, args).await,
200                VmValue::BuiltinRef(name) => {
201                    if !crate::autonomy::needs_async_side_effect_enforcement(name) {
202                        if let Some(result) = self.call_sync_builtin_by_ref(name, args) {
203                            return result;
204                        }
205                    }
206                    self.call_named_builtin(name, args.to_vec()).await
207                }
208                VmValue::BuiltinRefId { id, name } => {
209                    self.call_builtin_id_or_name(*id, name, args.to_vec()).await
210                }
211                other => Err(VmError::TypeError(format!(
212                    "expected callable, got {}",
213                    other.type_name()
214                ))),
215            }
216        })
217    }
218
219    fn call_sync_builtin_by_ref(
220        &mut self,
221        name: &str,
222        args: &[VmValue],
223    ) -> Option<Result<VmValue, VmError>> {
224        let builtin = self.builtins.get(name).cloned()?;
225
226        let span_kind = match name {
227            "llm_call" | "llm_stream" | "llm_stream_call" | "agent_loop" | "agent_turn" => {
228                Some(crate::tracing::SpanKind::LlmCall)
229            }
230            "mcp_call" => Some(crate::tracing::SpanKind::ToolCall),
231            _ => None,
232        };
233        let _span = span_kind.map(|kind| ScopeSpan::new(kind, name.to_string()));
234
235        if self.denied_builtins.contains(name) {
236            return Some(Err(VmError::CategorizedError {
237                message: format!("Tool '{}' is not permitted.", name),
238                category: ErrorCategory::ToolRejected,
239            }));
240        }
241        if let Err(err) = crate::orchestration::enforce_current_policy_for_builtin(name, args) {
242            return Some(Err(err));
243        }
244        if let Err(err) = crate::typecheck::validate_builtin_call(name, args, None) {
245            return Some(Err(err));
246        }
247
248        Some(builtin(args, &mut self.output))
249    }
250
251    /// Returns true if `v` is callable via `call_callable_value`.
252    pub(crate) fn is_callable_value(v: &VmValue) -> bool {
253        matches!(
254            v,
255            VmValue::Closure(_) | VmValue::BuiltinRef(_) | VmValue::BuiltinRefId { .. }
256        )
257    }
258
259    /// Public wrapper for `call_closure`, used by the MCP server to invoke
260    /// tool handler closures from outside the VM execution loop.
261    pub async fn call_closure_pub(
262        &mut self,
263        closure: &VmClosure,
264        args: &[VmValue],
265    ) -> Result<VmValue, VmError> {
266        self.cancel_grace_instructions_remaining = None;
267        self.call_closure(closure, args).await
268    }
269
270    /// Resolve a named builtin: sync builtins → async builtins → bridge → error.
271    /// Used by Call, TailCall, and Pipe handlers to avoid duplicating this lookup.
272    pub(crate) async fn call_named_builtin(
273        &mut self,
274        name: &str,
275        args: Vec<VmValue>,
276    ) -> Result<VmValue, VmError> {
277        self.call_builtin_impl(name, args, None).await
278    }
279
280    pub(crate) async fn call_builtin_id_or_name(
281        &mut self,
282        id: BuiltinId,
283        name: &str,
284        args: Vec<VmValue>,
285    ) -> Result<VmValue, VmError> {
286        self.call_builtin_impl(name, args, Some(id)).await
287    }
288
289    async fn call_builtin_impl(
290        &mut self,
291        name: &str,
292        args: Vec<VmValue>,
293        direct_id: Option<BuiltinId>,
294    ) -> Result<VmValue, VmError> {
295        // Auto-trace LLM calls and tool calls.
296        let span_kind = match name {
297            "llm_call" | "llm_stream" | "llm_stream_call" | "agent_loop" => {
298                Some(crate::tracing::SpanKind::LlmCall)
299            }
300            "mcp_call" => Some(crate::tracing::SpanKind::ToolCall),
301            _ => None,
302        };
303        let _span = span_kind.map(|kind| ScopeSpan::new(kind, name.to_string()));
304
305        // Sandbox check: deny builtins blocked by --deny/--allow flags.
306        if self.denied_builtins.contains(name) {
307            return Err(VmError::CategorizedError {
308                message: format!("Tool '{}' is not permitted.", name),
309                category: ErrorCategory::ToolRejected,
310            });
311        }
312        let autonomy = if crate::autonomy::needs_async_side_effect_enforcement(name) {
313            crate::autonomy::enforce_builtin_side_effect_boxed(name, &args).await?
314        } else {
315            None
316        };
317        if let Some(crate::autonomy::AutonomyDecision::Skip(value)) = autonomy {
318            return Ok(value);
319        }
320        if !matches!(
321            autonomy,
322            Some(crate::autonomy::AutonomyDecision::AllowApproved)
323        ) {
324            crate::orchestration::enforce_current_policy_for_builtin(name, &args)?;
325        }
326        crate::typecheck::validate_builtin_call(name, &args, None)?;
327
328        if let Some(result) =
329            crate::runtime_context::dispatch_runtime_context_builtin(self, name, &args)
330        {
331            return result;
332        }
333
334        if let Some(id) = direct_id {
335            if let Some(entry) = self.builtins_by_id.get(&id).cloned() {
336                if entry.name.as_ref() == name {
337                    return self.call_builtin_entry(entry.dispatch, args).await;
338                }
339            }
340        }
341
342        if let Some(builtin) = self.builtins.get(name).cloned() {
343            self.call_builtin_entry(VmBuiltinDispatch::Sync(builtin), args)
344                .await
345        } else if let Some(async_builtin) = self.async_builtins.get(name).cloned() {
346            self.call_builtin_entry(VmBuiltinDispatch::Async(async_builtin), args)
347                .await
348        } else if let Some(bridge) = &self.bridge {
349            crate::orchestration::enforce_current_policy_for_bridge_builtin(name)?;
350            let args_json: Vec<serde_json::Value> =
351                args.iter().map(crate::llm::vm_value_to_json).collect();
352            let result = bridge
353                .call(
354                    "builtin_call",
355                    serde_json::json!({"name": name, "args": args_json}),
356                )
357                .await?;
358            Ok(crate::bridge::json_result_to_vm_value(&result))
359        } else {
360            let all_builtins = self
361                .builtins
362                .keys()
363                .chain(self.async_builtins.keys())
364                .map(|s| s.as_str());
365            if let Some(suggestion) = crate::value::closest_match(name, all_builtins) {
366                return Err(VmError::Runtime(format!(
367                    "Undefined builtin: {name} (did you mean `{suggestion}`?)"
368                )));
369            }
370            Err(VmError::UndefinedBuiltin(name.to_string()))
371        }
372    }
373
374    async fn call_builtin_entry(
375        &mut self,
376        dispatch: VmBuiltinDispatch,
377        args: Vec<VmValue>,
378    ) -> Result<VmValue, VmError> {
379        match dispatch {
380            VmBuiltinDispatch::Sync(builtin) => builtin(&args, &mut self.output),
381            VmBuiltinDispatch::Async(async_builtin) => {
382                CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
383                    slot.borrow_mut().push(self.child_vm());
384                });
385                let result = async_builtin(args).await;
386                let captured = CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
387                    let mut stack = slot.borrow_mut();
388                    let mut top = stack.pop();
389                    top.as_mut().map(|vm| vm.take_output()).unwrap_or_default()
390                });
391                if !captured.is_empty() {
392                    self.output.push_str(&captured);
393                }
394                result
395            }
396        }
397    }
398}