Skip to main content

harn_vm/vm/
dispatch.rs

1use std::future::Future;
2use std::rc::Rc;
3
4use crate::value::{ErrorCategory, VmBuiltinFn, VmClosure, VmError, VmValue};
5use crate::BuiltinId;
6
7use super::async_builtin::CURRENT_ASYNC_BUILTIN_CHILD_VM;
8use super::{
9    CallArgs, ScopeSpan, Vm, VmBuiltinDispatch, VmBuiltinEntry, VmBuiltinKind, VmBuiltinMetadata,
10};
11
12impl Vm {
13    fn builtin_span_kind(name: &str) -> Option<crate::tracing::SpanKind> {
14        match name {
15            "llm_call" | "llm_stream" | "llm_stream_call" | "agent_loop" | "agent_turn" => {
16                Some(crate::tracing::SpanKind::LlmCall)
17            }
18            "mcp_call" => Some(crate::tracing::SpanKind::ToolCall),
19            _ => None,
20        }
21    }
22
23    fn is_runtime_context_builtin(name: &str) -> bool {
24        matches!(
25            name,
26            "runtime_context"
27                | "task_current"
28                | "runtime_context_values"
29                | "runtime_context_get"
30                | "runtime_context_set"
31                | "runtime_context_clear"
32        )
33    }
34
35    fn resolve_sync_builtin_id_or_name(
36        &self,
37        direct_id: Option<BuiltinId>,
38        name: &str,
39    ) -> Option<Result<VmBuiltinFn, VmError>> {
40        if crate::autonomy::needs_async_side_effect_enforcement(name)
41            || Self::is_runtime_context_builtin(name)
42        {
43            return None;
44        }
45
46        let dispatch = if let Some(id) = direct_id {
47            self.builtins_by_id
48                .get(&id)
49                .filter(|entry| entry.name.as_ref() == name)
50                .map(|entry| entry.dispatch.clone())
51        } else {
52            None
53        }
54        .or_else(|| {
55            self.builtins
56                .get(name)
57                .cloned()
58                .map(VmBuiltinDispatch::Sync)
59        });
60
61        let Some(dispatch) = dispatch else {
62            if self.async_builtins.contains_key(name) || self.bridge.is_some() {
63                return None;
64            }
65            let all_builtins = self
66                .builtins
67                .keys()
68                .chain(self.async_builtins.keys())
69                .map(|s| s.as_str());
70            return Some(
71                if let Some(suggestion) = crate::value::closest_match(name, all_builtins) {
72                    Err(VmError::Runtime(format!(
73                        "Undefined builtin: {name} (did you mean `{suggestion}`?)"
74                    )))
75                } else {
76                    Err(VmError::UndefinedBuiltin(name.to_string()))
77                },
78            );
79        };
80
81        match dispatch {
82            VmBuiltinDispatch::Sync(builtin) => Some(Ok(builtin)),
83            VmBuiltinDispatch::Async(_) => None,
84        }
85    }
86
87    fn validate_sync_builtin_args(&self, name: &str, args: &[VmValue]) -> Result<(), VmError> {
88        if self.denied_builtins.contains(name) {
89            return Err(VmError::CategorizedError {
90                message: format!("Tool '{name}' is not permitted."),
91                category: ErrorCategory::ToolRejected,
92            });
93        }
94        crate::orchestration::enforce_current_policy_for_builtin(name, args)?;
95        crate::typecheck::validate_builtin_call(name, args, None)
96    }
97
98    fn index_builtin_id(&mut self, name: &str, dispatch: VmBuiltinDispatch) {
99        let id = BuiltinId::from_name(name);
100        if self.builtin_id_collisions.contains(&id) {
101            return;
102        }
103        if let Some(existing) = self.builtins_by_id.get(&id) {
104            if existing.name.as_ref() != name {
105                Rc::make_mut(&mut self.builtins_by_id).remove(&id);
106                Rc::make_mut(&mut self.builtin_id_collisions).insert(id);
107                return;
108            }
109        }
110        Rc::make_mut(&mut self.builtins_by_id).insert(
111            id,
112            VmBuiltinEntry {
113                name: Rc::from(name),
114                dispatch,
115            },
116        );
117    }
118
119    fn refresh_builtin_id(&mut self, name: &str) {
120        if let Some(builtin) = self.builtins.get(name).cloned() {
121            self.index_builtin_id(name, VmBuiltinDispatch::Sync(builtin));
122        } else if let Some(async_builtin) = self.async_builtins.get(name).cloned() {
123            self.index_builtin_id(name, VmBuiltinDispatch::Async(async_builtin));
124        } else {
125            let id = BuiltinId::from_name(name);
126            if self
127                .builtins_by_id
128                .get(&id)
129                .is_some_and(|entry| entry.name.as_ref() == name)
130            {
131                Rc::make_mut(&mut self.builtins_by_id).remove(&id);
132            }
133        }
134    }
135
136    /// Register a sync builtin function.
137    pub fn register_builtin<F>(&mut self, name: &str, f: F)
138    where
139        F: Fn(&[VmValue], &mut String) -> Result<VmValue, VmError> + 'static,
140    {
141        Rc::make_mut(&mut self.builtins).insert(name.to_string(), Rc::new(f));
142        Rc::make_mut(&mut self.builtin_metadata)
143            .insert(name.to_string(), VmBuiltinMetadata::sync(name.to_string()));
144        Rc::make_mut(&mut self.deferred_builtin_registrars).remove(name);
145        self.refresh_builtin_id(name);
146    }
147
148    /// Register a sync builtin function with discoverable metadata.
149    pub fn register_builtin_with_metadata<F>(&mut self, metadata: VmBuiltinMetadata, f: F)
150    where
151        F: Fn(&[VmValue], &mut String) -> Result<VmValue, VmError> + 'static,
152    {
153        let name = metadata.name().to_string();
154        Rc::make_mut(&mut self.builtins).insert(name.clone(), Rc::new(f));
155        Rc::make_mut(&mut self.builtin_metadata)
156            .insert(name.clone(), metadata.with_kind(VmBuiltinKind::Sync));
157        Rc::make_mut(&mut self.deferred_builtin_registrars).remove(&name);
158        self.refresh_builtin_id(&name);
159    }
160
161    /// Register a `VmBuiltinDef` (the shape emitted by `#[harn_builtin]`).
162    /// Registers the primary name plus each declared alias, sharing the
163    /// same handler. `runtime_only` defs skip the parser-side publish (the
164    /// vm-side registration still happens). `parser_only` defs skip the
165    /// vm-side registration entirely (handler is `None`).
166    pub fn register_builtin_def(&mut self, def: &'static crate::stdlib::macros::VmBuiltinDef) {
167        use crate::stdlib::macros::VmBuiltinHandler;
168        if def.parser_only {
169            return;
170        }
171        let names = std::iter::once(def.sig.name).chain(def.aliases.iter().copied());
172        for name in names {
173            match def.handler {
174                VmBuiltinHandler::Sync(f) => {
175                    let mut meta = VmBuiltinMetadata::sync_static(name);
176                    if let Some(category) = def.category {
177                        meta = meta.category_static(category);
178                    }
179                    if let Some(doc) = def.doc {
180                        meta = meta.doc_static(doc);
181                    }
182                    self.register_builtin_with_metadata(meta, f);
183                }
184                VmBuiltinHandler::Async(f) => {
185                    let mut meta = VmBuiltinMetadata::async_static(name);
186                    if let Some(category) = def.category {
187                        meta = meta.category_static(category);
188                    }
189                    if let Some(doc) = def.doc {
190                        meta = meta.doc_static(doc);
191                    }
192                    // Wrap the function pointer that already returns an
193                    // AsyncBuiltinFuture so register_async_builtin_with_metadata's
194                    // generic bound `F: Fn(Vec<VmValue>) -> Fut + 'static` is met.
195                    self.register_async_builtin_with_metadata(meta, f);
196                }
197                VmBuiltinHandler::None => {
198                    // Parser-only, but reached here despite parser_only=false.
199                    // This is a configuration bug.
200                    panic!(
201                        "VmBuiltinHandler::None for {name:?} without parser_only=true \
202                         on its BuiltinDef"
203                    );
204                }
205            }
206        }
207    }
208
209    /// Remove a sync builtin (so an async version can take precedence).
210    pub fn unregister_builtin(&mut self, name: &str) {
211        Rc::make_mut(&mut self.builtins).remove(name);
212        if self.async_builtins.contains_key(name) {
213            Rc::make_mut(&mut self.builtin_metadata).insert(
214                name.to_string(),
215                VmBuiltinMetadata::async_builtin(name.to_string()),
216            );
217        } else {
218            Rc::make_mut(&mut self.builtin_metadata).remove(name);
219        }
220        self.refresh_builtin_id(name);
221    }
222
223    /// Register an async builtin function.
224    pub fn register_async_builtin<F, Fut>(&mut self, name: &str, f: F)
225    where
226        F: Fn(Vec<VmValue>) -> Fut + 'static,
227        Fut: Future<Output = Result<VmValue, VmError>> + 'static,
228    {
229        Rc::make_mut(&mut self.async_builtins)
230            .insert(name.to_string(), Rc::new(move |args| Box::pin(f(args))));
231        Rc::make_mut(&mut self.builtin_metadata).insert(
232            name.to_string(),
233            VmBuiltinMetadata::async_builtin(name.to_string()),
234        );
235        Rc::make_mut(&mut self.deferred_builtin_registrars).remove(name);
236        self.refresh_builtin_id(name);
237    }
238
239    /// Register an async builtin function with discoverable metadata.
240    pub fn register_async_builtin_with_metadata<F, Fut>(
241        &mut self,
242        metadata: VmBuiltinMetadata,
243        f: F,
244    ) where
245        F: Fn(Vec<VmValue>) -> Fut + 'static,
246        Fut: Future<Output = Result<VmValue, VmError>> + 'static,
247    {
248        let name = metadata.name().to_string();
249        Rc::make_mut(&mut self.async_builtins)
250            .insert(name.clone(), Rc::new(move |args| Box::pin(f(args))));
251        Rc::make_mut(&mut self.builtin_metadata)
252            .insert(name.clone(), metadata.with_kind(VmBuiltinKind::Async));
253        Rc::make_mut(&mut self.deferred_builtin_registrars).remove(&name);
254        self.refresh_builtin_id(&name);
255    }
256
257    /// Register a builtin name whose implementation should be installed only
258    /// if a script actually resolves that name.
259    pub(crate) fn register_deferred_builtin(&mut self, name: &str, registrar: fn(&mut Vm)) {
260        if self.builtins.contains_key(name) || self.async_builtins.contains_key(name) {
261            return;
262        }
263        Rc::make_mut(&mut self.deferred_builtin_registrars).insert(name.to_string(), registrar);
264    }
265
266    pub(crate) fn ensure_deferred_builtin(&mut self, name: &str) -> bool {
267        let Some(registrar) = self.deferred_builtin_registrars.get(name).copied() else {
268            return false;
269        };
270        registrar(self);
271        Rc::make_mut(&mut self.deferred_builtin_registrars).remove(name);
272        self.builtins.contains_key(name) || self.async_builtins.contains_key(name)
273    }
274
275    pub(crate) fn registered_builtin_id(&self, name: &str) -> Option<BuiltinId> {
276        let id = BuiltinId::from_name(name);
277        if self
278            .builtins_by_id
279            .get(&id)
280            .is_some_and(|entry| entry.name.as_ref() == name)
281        {
282            Some(id)
283        } else {
284            None
285        }
286    }
287
288    /// Invoke a closure inline against the existing VM frame stack.
289    ///
290    /// Dispatch path for every callback-taking method on lists/dicts/sets
291    /// (`.map`, `.filter`, `.reduce`, `.each`, `.sort_by`, …) via
292    /// [`call_callable_value`]. The closure's frame is pushed onto
293    /// `self.frames` using the same machinery as `Op::Call`, and the
294    /// shared dispatch loop ([`Vm::drive_until_frame_depth`]) drains the
295    /// sub-execution back to the caller's depth.
296    ///
297    /// This avoids the per-invocation `Pin<Box<dyn Future>>` heap
298    /// allocation a recursive `async fn` would require — the recursion
299    /// cycle (closure → `.map` → callback → closure) is broken instead at
300    /// [`Vm::call_method`], which keeps a single boxed future per
301    /// method-call site rather than per callback element.
302    ///
303    /// Exception handlers are saved and cleared before the sub-execution
304    /// so an unhandled throw inside the body propagates as a Rust
305    /// `Result::Err` to the caller's dispatch loop. Iterators, deadlines,
306    /// and frames are scoped by `CallFrame::saved_iterator_depth` and the
307    /// per-frame deadline tags.
308    pub(crate) async fn call_closure(
309        &mut self,
310        closure: &VmClosure,
311        args: &[VmValue],
312    ) -> Result<VmValue, VmError> {
313        self.call_closure_args(closure, CallArgs::Slice(args)).await
314    }
315
316    pub(crate) async fn call_closure_args(
317        &mut self,
318        closure: &VmClosure,
319        args: CallArgs<'_>,
320    ) -> Result<VmValue, VmError> {
321        let saved_handlers = std::mem::take(&mut self.exception_handlers);
322        let active_context = (!crate::step_runtime::is_tracked_function(&closure.func.name))
323            .then(crate::step_runtime::take_active_context);
324
325        let target_frame_depth = self.frames.len();
326        let frame_result = self.push_closure_frame_args(closure, &args);
327        drop(args);
328        let result = match frame_result {
329            Ok(()) => self.drive_until_frame_depth(target_frame_depth).await,
330            Err(e) => Err(e),
331        };
332
333        self.exception_handlers = saved_handlers;
334        if let Some(ctx) = active_context {
335            crate::step_runtime::restore_active_context(ctx);
336        }
337
338        result
339    }
340
341    /// Invoke a value as a callable. Supports `VmValue::Closure` and
342    /// `VmValue::BuiltinRef`, so builtin names passed by reference (e.g.
343    /// `dict.rekey(snake_to_camel)`) dispatch through the same code path as
344    /// user-defined closures.
345    pub(crate) async fn call_callable_value(
346        &mut self,
347        callable: &VmValue,
348        args: &[VmValue],
349    ) -> Result<VmValue, VmError> {
350        self.call_callable_args(callable, CallArgs::Slice(args))
351            .await
352    }
353
354    pub(crate) async fn call_callable_owned(
355        &mut self,
356        callable: &VmValue,
357        args: Vec<VmValue>,
358    ) -> Result<VmValue, VmError> {
359        self.call_callable_args(callable, CallArgs::Owned(args))
360            .await
361    }
362
363    pub(crate) async fn call_callable_zero(
364        &mut self,
365        callable: &VmValue,
366    ) -> Result<VmValue, VmError> {
367        self.call_callable_args(callable, CallArgs::Empty).await
368    }
369
370    pub(crate) async fn call_callable_one(
371        &mut self,
372        callable: &VmValue,
373        arg: &VmValue,
374    ) -> Result<VmValue, VmError> {
375        self.call_callable_args(callable, CallArgs::One(arg)).await
376    }
377
378    pub(crate) async fn call_callable_two(
379        &mut self,
380        callable: &VmValue,
381        first: &VmValue,
382        second: &VmValue,
383    ) -> Result<VmValue, VmError> {
384        self.call_callable_args(callable, CallArgs::Two(first, second))
385            .await
386    }
387
388    pub(crate) async fn call_callable_args(
389        &mut self,
390        callable: &VmValue,
391        args: CallArgs<'_>,
392    ) -> Result<VmValue, VmError> {
393        match callable {
394            VmValue::Closure(closure) => self.call_closure_args(closure, args).await,
395            VmValue::BuiltinRef(name) => {
396                if !crate::autonomy::needs_async_side_effect_enforcement(name) {
397                    if let Some(result) = self.call_sync_builtin_by_ref_args(name, &args) {
398                        return result;
399                    }
400                }
401                self.call_named_builtin(name, args.into_vec()).await
402            }
403            VmValue::BuiltinRefId { id, name } => {
404                if let Some(result) =
405                    self.try_call_sync_builtin_id_or_name_args(Some(*id), name, &args)
406                {
407                    return result;
408                }
409                self.call_builtin_id_or_name(*id, name, args.into_vec())
410                    .await
411            }
412            other => Err(VmError::TypeError(format!(
413                "expected callable, got {}",
414                other.type_name()
415            ))),
416        }
417    }
418
419    fn call_sync_builtin_by_ref_args(
420        &mut self,
421        name: &str,
422        args: &CallArgs<'_>,
423    ) -> Option<Result<VmValue, VmError>> {
424        self.try_call_sync_builtin_id_or_name_args(None, name, args)
425    }
426
427    /// Returns true if `v` is callable via `call_callable_value`.
428    pub(crate) fn is_callable_value(v: &VmValue) -> bool {
429        matches!(
430            v,
431            VmValue::Closure(_) | VmValue::BuiltinRef(_) | VmValue::BuiltinRefId { .. }
432        )
433    }
434
435    /// Public wrapper for `call_closure`, used by the MCP server to invoke
436    /// tool handler closures from outside the VM execution loop.
437    pub async fn call_closure_pub(
438        &mut self,
439        closure: &VmClosure,
440        args: &[VmValue],
441    ) -> Result<VmValue, VmError> {
442        self.cancel_grace_instructions_remaining = None;
443        self.call_closure(closure, args).await
444    }
445
446    /// Resolve a named builtin: sync builtins → async builtins → bridge → error.
447    /// Used by Call, TailCall, and Pipe handlers to avoid duplicating this lookup.
448    pub(crate) async fn call_named_builtin(
449        &mut self,
450        name: &str,
451        args: Vec<VmValue>,
452    ) -> Result<VmValue, VmError> {
453        self.call_builtin_impl(name, args, None).await
454    }
455
456    pub(crate) async fn call_builtin_id_or_name(
457        &mut self,
458        id: BuiltinId,
459        name: &str,
460        args: Vec<VmValue>,
461    ) -> Result<VmValue, VmError> {
462        self.call_builtin_impl(name, args, Some(id)).await
463    }
464
465    pub(crate) fn try_call_sync_builtin_id_or_name_args(
466        &mut self,
467        direct_id: Option<BuiltinId>,
468        name: &str,
469        args: &CallArgs<'_>,
470    ) -> Option<Result<VmValue, VmError>> {
471        if self.denied_builtins.contains(name) {
472            return Some(Err(VmError::CategorizedError {
473                message: format!("Tool '{name}' is not permitted."),
474                category: ErrorCategory::ToolRejected,
475            }));
476        }
477        self.ensure_deferred_builtin(name);
478        let builtin = match self.resolve_sync_builtin_id_or_name(direct_id, name)? {
479            Ok(builtin) => builtin,
480            Err(error) => return Some(Err(error)),
481        };
482        let _span =
483            Self::builtin_span_kind(name).map(|kind| ScopeSpan::new(kind, name.to_string()));
484        if let Err(error) = args.with_slice(|slice| self.validate_sync_builtin_args(name, slice)) {
485            return Some(Err(error));
486        }
487
488        Some(args.with_slice(|slice| builtin(slice, &mut self.output)))
489    }
490
491    pub(crate) fn try_call_sync_builtin_id_or_name_from_stack_args(
492        &mut self,
493        direct_id: Option<BuiltinId>,
494        name: &str,
495        args_start: usize,
496    ) -> Option<Result<VmValue, VmError>> {
497        if self.denied_builtins.contains(name) {
498            return Some(Err(VmError::CategorizedError {
499                message: format!("Tool '{name}' is not permitted."),
500                category: ErrorCategory::ToolRejected,
501            }));
502        }
503        self.ensure_deferred_builtin(name);
504        let builtin = match self.resolve_sync_builtin_id_or_name(direct_id, name)? {
505            Ok(builtin) => builtin,
506            Err(error) => return Some(Err(error)),
507        };
508        if args_start > self.stack.len() {
509            return Some(Err(VmError::Runtime(
510                "call argument stack underflow".to_string(),
511            )));
512        }
513
514        let _span =
515            Self::builtin_span_kind(name).map(|kind| ScopeSpan::new(kind, name.to_string()));
516        let args = &self.stack[args_start..];
517        if let Err(error) = self.validate_sync_builtin_args(name, args) {
518            return Some(Err(error));
519        }
520
521        Some(builtin(args, &mut self.output))
522    }
523
524    async fn call_builtin_impl(
525        &mut self,
526        name: &str,
527        args: Vec<VmValue>,
528        direct_id: Option<BuiltinId>,
529    ) -> Result<VmValue, VmError> {
530        // Auto-trace LLM calls and tool calls.
531        let _span =
532            Self::builtin_span_kind(name).map(|kind| ScopeSpan::new(kind, name.to_string()));
533
534        // Sandbox check: deny builtins blocked by --deny/--allow flags.
535        if self.denied_builtins.contains(name) {
536            return Err(VmError::CategorizedError {
537                message: format!("Tool '{name}' is not permitted."),
538                category: ErrorCategory::ToolRejected,
539            });
540        }
541        let autonomy = if crate::autonomy::needs_async_side_effect_enforcement(name) {
542            crate::autonomy::enforce_builtin_side_effect_boxed(name, &args).await?
543        } else {
544            None
545        };
546        if let Some(crate::autonomy::AutonomyDecision::Skip(value)) = autonomy {
547            return Ok(value);
548        }
549        if !matches!(
550            autonomy,
551            Some(crate::autonomy::AutonomyDecision::AllowApproved)
552        ) {
553            crate::orchestration::enforce_current_policy_for_builtin(name, &args)?;
554        }
555        crate::typecheck::validate_builtin_call(name, &args, None)?;
556
557        if let Some(result) =
558            crate::runtime_context::dispatch_runtime_context_builtin(self, name, &args)
559        {
560            return result;
561        }
562
563        self.ensure_deferred_builtin(name);
564
565        if let Some(id) = direct_id {
566            if let Some(entry) = self.builtins_by_id.get(&id).cloned() {
567                if entry.name.as_ref() == name {
568                    return self.call_builtin_entry(entry.dispatch, args).await;
569                }
570            }
571        }
572
573        if let Some(builtin) = self.builtins.get(name).cloned() {
574            self.call_builtin_entry(VmBuiltinDispatch::Sync(builtin), args)
575                .await
576        } else if let Some(async_builtin) = self.async_builtins.get(name).cloned() {
577            self.call_builtin_entry(VmBuiltinDispatch::Async(async_builtin), args)
578                .await
579        } else if let Some(bridge) = &self.bridge {
580            crate::orchestration::enforce_current_policy_for_bridge_builtin(name)?;
581            let args_json: Vec<serde_json::Value> =
582                args.iter().map(crate::llm::vm_value_to_json).collect();
583            let result = bridge
584                .call(
585                    "builtin_call",
586                    serde_json::json!({"name": name, "args": args_json}),
587                )
588                .await?;
589            Ok(crate::bridge::json_result_to_vm_value(&result))
590        } else {
591            let all_builtins = self
592                .builtins
593                .keys()
594                .chain(self.async_builtins.keys())
595                .chain(self.deferred_builtin_registrars.keys())
596                .map(|s| s.as_str());
597            if let Some(suggestion) = crate::value::closest_match(name, all_builtins) {
598                return Err(VmError::Runtime(format!(
599                    "Undefined builtin: {name} (did you mean `{suggestion}`?)"
600                )));
601            }
602            Err(VmError::UndefinedBuiltin(name.to_string()))
603        }
604    }
605
606    async fn call_builtin_entry(
607        &mut self,
608        dispatch: VmBuiltinDispatch,
609        args: Vec<VmValue>,
610    ) -> Result<VmValue, VmError> {
611        match dispatch {
612            VmBuiltinDispatch::Sync(builtin) => builtin(&args, &mut self.output),
613            VmBuiltinDispatch::Async(async_builtin) => {
614                CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
615                    slot.borrow_mut().push(self.child_vm());
616                });
617                let result = async_builtin(args).await;
618                let captured = CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
619                    let mut stack = slot.borrow_mut();
620                    let mut top = stack.pop();
621                    top.as_mut().map(|vm| vm.take_output()).unwrap_or_default()
622                });
623                if !captured.is_empty() {
624                    self.output.push_str(&captured);
625                }
626                result
627            }
628        }
629    }
630}