Skip to main content

harn_vm/vm/
dispatch.rs

1use std::future::Future;
2use std::sync::Arc;
3
4use crate::value::{ErrorCategory, VmBuiltinFn, VmClosure, VmError, VmValue};
5use crate::BuiltinId;
6
7use super::{
8    CallArgs, ScopeSpan, Vm, VmBuiltinArity, VmBuiltinDispatch, VmBuiltinEntry, VmBuiltinKind,
9    VmBuiltinMetadata,
10};
11
12/// Everything that watches one builtin call, held open for its duration.
13///
14/// A builtin reaches its handler through one of three paths (two sync fast
15/// paths that differ only in where the arguments live, and the async/bridge
16/// path). Each used to open the auto-trace span itself, so an observer added to
17/// one path silently missed the other two — per-builtin cost recording was
18/// added to the async path first and reported nothing, because a `let` binding
19/// in the arm nobody takes looks exactly like a working one.
20///
21/// Opening this is the one thing a dispatch path must do before invoking a
22/// handler. New observers belong here, not at a call site.
23struct BuiltinObservation<'a> {
24    _span: Option<ScopeSpan>,
25    _timer: Option<crate::builtin_profile::BuiltinTimer<'a>>,
26}
27
28struct ResolvedSyncBuiltin {
29    handler: VmBuiltinFn,
30    recorded_effects: Option<&'static [harn_builtin_meta::EffectSpec]>,
31}
32
33impl Vm {
34    fn builtin_span_kind(name: &str) -> Option<crate::tracing::SpanKind> {
35        match name {
36            "llm_call" | "llm_stream" | "llm_stream_call" | "agent_loop" | "agent_turn" => {
37                Some(crate::tracing::SpanKind::LlmCall)
38            }
39            "mcp_call" => Some(crate::tracing::SpanKind::ToolCall),
40            _ => None,
41        }
42    }
43
44    /// Open the observation scope for one builtin call. Both members are inert
45    /// unless the operator asked for the corresponding output.
46    ///
47    /// The guard is returned BOXED, and only when something is actually being
48    /// observed. A builtin call reaches the VM recursively (a builtin invokes a
49    /// pipeline that dispatches more builtins), so this guard is a live local on
50    /// every frame of that recursion. Held by value, the ~48-byte aggregate did
51    /// not just add its own size per frame — as a recursive-frame local it
52    /// shifted the compiler's spill/inline decisions, so the real growth
53    /// exceeded `size_of::<BuiltinObservation>()` and overflowed the stack on
54    /// deep dispatch even with profiling OFF (both members `None`). Returning
55    /// `Option<Box<_>>` keeps the frame local pointer-sized (an 8-byte niche
56    /// `None` on the inert hot path, with no allocation) and only touches the
57    /// heap when an observer is genuinely active. See harn#4928.
58    fn observe_builtin_call(name: &str) -> Option<Box<BuiltinObservation<'_>>> {
59        let span = Self::builtin_span_kind(name).map(|kind| ScopeSpan::new(kind, name.to_string()));
60        let timer = crate::builtin_profile::BuiltinTimer::start(name);
61        if span.is_none() && timer.is_none() {
62            // Inert: nothing to observe. No allocation, an 8-byte `None` local.
63            return None;
64        }
65        Some(Box::new(BuiltinObservation {
66            _span: span,
67            _timer: timer,
68        }))
69    }
70
71    fn is_runtime_context_builtin(name: &str) -> bool {
72        matches!(
73            name,
74            "runtime_context"
75                | "task_current"
76                | "runtime_context_values"
77                | "runtime_context_get"
78                | "runtime_context_set"
79                | "runtime_context_clear"
80        )
81    }
82
83    fn resolve_sync_builtin_id_or_name(
84        &self,
85        direct_id: Option<BuiltinId>,
86        name: &str,
87    ) -> Option<Result<ResolvedSyncBuiltin, VmError>> {
88        if crate::autonomy::needs_async_side_effect_enforcement(name)
89            || Self::is_runtime_context_builtin(name)
90        {
91            return None;
92        }
93
94        let dispatch = if let Some(id) = direct_id {
95            self.builtins_by_id
96                .get(&id)
97                .filter(|entry| entry.name.as_ref() == name)
98                .map(|entry| (entry.dispatch.clone(), entry.recorded_effects))
99        } else {
100            None
101        }
102        .or_else(|| {
103            self.builtins.get(name).cloned().map(|builtin| {
104                let recorded_effects = crate::stdlib::recorded_effect_builtin_manifest_entry(name)
105                    .map(|entry| entry.contract.effects);
106                (VmBuiltinDispatch::Sync(builtin), recorded_effects)
107            })
108        });
109
110        let Some(dispatch) = dispatch else {
111            if self.async_builtins.contains_key(name) || self.bridge.is_some() {
112                return None;
113            }
114            let all_builtins = self
115                .builtins
116                .keys()
117                .chain(self.async_builtins.keys())
118                .map(|s| s.as_str());
119            return Some(
120                if let Some(suggestion) = crate::value::closest_match(name, all_builtins) {
121                    Err(VmError::Runtime(format!(
122                        "Undefined builtin: {name} (did you mean `{suggestion}`?)"
123                    )))
124                } else {
125                    Err(VmError::UndefinedBuiltin(name.to_string()))
126                },
127            );
128        };
129
130        match dispatch {
131            (VmBuiltinDispatch::Sync(builtin), recorded_effects) => Some(Ok(ResolvedSyncBuiltin {
132                handler: builtin,
133                recorded_effects,
134            })),
135            (VmBuiltinDispatch::Async(_), _) => None,
136        }
137    }
138
139    fn validate_sync_builtin_args(
140        &self,
141        name: &str,
142        args: &[VmValue],
143        recorded_effects: Option<&'static [harn_builtin_meta::EffectSpec]>,
144    ) -> Result<(), VmError> {
145        if self.denied_builtins.contains(name) {
146            return Err(VmError::CategorizedError {
147                message: format!("Tool '{name}' is not permitted."),
148                category: ErrorCategory::ToolRejected,
149            });
150        }
151        crate::orchestration::enforce_current_policy_for_builtin(name, args)?;
152        if let Some(specs) = recorded_effects {
153            self.record_builtin_effect_specs(specs, args);
154        }
155        crate::typecheck::validate_builtin_call(name, args, None)
156    }
157
158    fn index_builtin_id(&mut self, name: &str, dispatch: VmBuiltinDispatch) {
159        let id = BuiltinId::from_name(name);
160        if self.builtin_id_collisions.contains(&id) {
161            return;
162        }
163        if let Some(existing) = self.builtins_by_id.get(&id) {
164            if existing.name.as_ref() != name {
165                Arc::make_mut(&mut self.builtins_by_id).remove(&id);
166                Arc::make_mut(&mut self.builtin_id_collisions).insert(id);
167                return;
168            }
169        }
170        Arc::make_mut(&mut self.builtins_by_id).insert(
171            id,
172            VmBuiltinEntry {
173                name: std::sync::Arc::from(name),
174                dispatch,
175                recorded_effects: crate::stdlib::recorded_effect_builtin_manifest_entry(name)
176                    .map(|entry| entry.contract.effects),
177            },
178        );
179    }
180
181    fn refresh_builtin_id(&mut self, name: &str) {
182        if let Some(builtin) = self.builtins.get(name).cloned() {
183            self.index_builtin_id(name, VmBuiltinDispatch::Sync(builtin));
184        } else if let Some(async_builtin) = self.async_builtins.get(name).cloned() {
185            self.index_builtin_id(name, VmBuiltinDispatch::Async(async_builtin));
186        } else {
187            let id = BuiltinId::from_name(name);
188            if self
189                .builtins_by_id
190                .get(&id)
191                .is_some_and(|entry| entry.name.as_ref() == name)
192            {
193                Arc::make_mut(&mut self.builtins_by_id).remove(&id);
194            }
195        }
196    }
197
198    /// Register a sync builtin function.
199    pub fn register_builtin<F>(&mut self, name: &str, f: F)
200    where
201        F: Fn(&[VmValue], &mut String) -> Result<VmValue, VmError> + Send + Sync + 'static,
202    {
203        Arc::make_mut(&mut self.builtins).insert(name.to_string(), Arc::new(f));
204        Arc::make_mut(&mut self.builtin_metadata)
205            .insert(name.to_string(), VmBuiltinMetadata::sync(name.to_string()));
206        self.refresh_builtin_id(name);
207    }
208
209    /// Register a dynamically supplied sync builtin with a complete typed
210    /// source exposure/effect contract.
211    pub fn register_builtin_with_contract<F>(
212        &mut self,
213        name: &str,
214        contract: harn_builtin_meta::BuiltinContract,
215        f: F,
216    ) where
217        F: Fn(&[VmValue], &mut String) -> Result<VmValue, VmError> + Send + Sync + 'static,
218    {
219        self.register_builtin_with_metadata(
220            VmBuiltinMetadata::sync(name.to_string()).with_contract(contract),
221            f,
222        );
223    }
224
225    /// Register a sync builtin function with discoverable metadata.
226    pub fn register_builtin_with_metadata<F>(&mut self, metadata: VmBuiltinMetadata, f: F)
227    where
228        F: Fn(&[VmValue], &mut String) -> Result<VmValue, VmError> + Send + Sync + 'static,
229    {
230        let name = metadata.name().to_string();
231        Arc::make_mut(&mut self.builtins).insert(name.clone(), Arc::new(f));
232        Arc::make_mut(&mut self.builtin_metadata)
233            .insert(name.clone(), metadata.with_kind(VmBuiltinKind::Sync));
234        self.refresh_builtin_id(&name);
235    }
236
237    /// Register a `VmBuiltinDef` (the shape emitted by `#[harn_builtin]`).
238    /// Registers the primary name plus each declared alias, sharing the
239    /// same handler. `runtime_only` defs skip the parser-side publish (the
240    /// vm-side registration still happens). `parser_only` defs skip the
241    /// vm-side registration entirely (handler is `None`).
242    pub fn register_builtin_def(&mut self, def: &'static crate::stdlib::macros::VmBuiltinDef) {
243        use crate::stdlib::macros::VmBuiltinHandler;
244        if def.parser_only {
245            return;
246        }
247        // Derive arity from the parsed `BuiltinSignature` so the discoverable
248        // metadata layer (harn explain, alignment-test metadata check) keeps
249        // parity with the pre-macro DSL builder.
250        let arity = arity_from_sig(&def.sig);
251        let names = std::iter::once(def.sig.name).chain(def.aliases.iter().copied());
252        for name in names {
253            match def.handler {
254                VmBuiltinHandler::Sync(f) => {
255                    let meta = builtin_def_metadata(def, name, arity, VmBuiltinKind::Sync);
256                    self.register_builtin_with_metadata(meta, f);
257                }
258                VmBuiltinHandler::Async(f) => {
259                    let meta = builtin_def_metadata(def, name, arity, VmBuiltinKind::Async);
260                    // Wrap the function pointer that already returns an
261                    // AsyncBuiltinFuture so register_async_builtin_with_metadata's
262                    // generic handler/future bounds are met.
263                    self.register_async_builtin_with_metadata(meta, f);
264                }
265                VmBuiltinHandler::None => {
266                    // Parser-only, but reached here despite parser_only=false.
267                    // This is a configuration bug.
268                    panic!(
269                        "VmBuiltinHandler::None for {name:?} without parser_only=true \
270                         on its BuiltinDef"
271                    );
272                }
273            }
274        }
275    }
276
277    /// Project macro-declared Harness methods onto the runtime dispatch table.
278    ///
279    /// Modules may still install a specialized adapter explicitly. For the
280    /// ordinary case, however, the builtin contract is the single source of
281    /// truth: declaring `exposure = "harness.<capability>.<method>"` is enough
282    /// to make that handler callable through the typed capability and never
283    /// creates a second hand-maintained registration list.
284    pub(crate) fn project_declared_capability_methods(&mut self) {
285        use harn_builtin_meta::BuiltinExposure;
286
287        let projections = self
288            .builtin_metadata
289            .iter()
290            .filter_map(|(name, metadata)| {
291                let BuiltinExposure::HarnessMethod { capability, method } =
292                    metadata.contract().exposure
293                else {
294                    return None;
295                };
296                Some((capability, method, name.clone(), metadata.kind()))
297            })
298            .collect::<Vec<_>>();
299
300        for (capability, method, name, kind) in projections {
301            if self
302                .capability_methods
303                .get(&capability)
304                .is_some_and(|methods| methods.contains_key(method))
305            {
306                continue;
307            }
308            let dispatch = match kind {
309                VmBuiltinKind::Sync => self
310                    .builtins
311                    .get(name.as_str())
312                    .cloned()
313                    .map(VmBuiltinDispatch::Sync),
314                VmBuiltinKind::Async => self
315                    .async_builtins
316                    .get(name.as_str())
317                    .cloned()
318                    .map(VmBuiltinDispatch::Async),
319            }
320            .unwrap_or_else(|| {
321                panic!(
322                    "declared capability method harness.{}.{method} has no runtime handler `{name}`",
323                    capability.field_name()
324                )
325            });
326            Arc::make_mut(&mut self.capability_methods)
327                .entry(capability)
328                .or_default()
329                .insert(method.to_string(), dispatch);
330        }
331    }
332
333    /// Restore unqualified capability-method calls for an explicitly opted-in
334    /// legacy process. Only uniquely owned method names are projected; a name
335    /// shared by two capabilities remains unavailable.
336    pub(crate) fn project_legacy_capability_globals(&mut self) {
337        if self.global("harness").is_none() {
338            return;
339        }
340        let mut projections =
341            std::collections::BTreeMap::<String, Option<VmBuiltinDispatch>>::new();
342        for methods in self.capability_methods.values() {
343            for (method, dispatch) in methods {
344                projections
345                    .entry(method.clone())
346                    .and_modify(|entry| *entry = None)
347                    .or_insert_with(|| Some(dispatch.clone()));
348            }
349        }
350        for (method, dispatch) in projections {
351            if self.builtins.contains_key(&method) || self.async_builtins.contains_key(&method) {
352                continue;
353            }
354            match dispatch {
355                Some(VmBuiltinDispatch::Sync(handler)) => {
356                    self.register_builtin(&method, move |args, output| handler(args, output));
357                }
358                Some(VmBuiltinDispatch::Async(handler)) => {
359                    self.register_async_builtin(&method, move |ctx, args| handler(ctx, args));
360                }
361                None => {}
362            }
363        }
364    }
365
366    /// Remove a sync builtin (so an async version can take precedence).
367    pub fn unregister_builtin(&mut self, name: &str) {
368        Arc::make_mut(&mut self.builtins).remove(name);
369        if self.async_builtins.contains_key(name) {
370            Arc::make_mut(&mut self.builtin_metadata).insert(
371                name.to_string(),
372                VmBuiltinMetadata::async_builtin(name.to_string()),
373            );
374        } else {
375            Arc::make_mut(&mut self.builtin_metadata).remove(name);
376        }
377        self.refresh_builtin_id(name);
378    }
379
380    /// Register an async builtin function. The handler receives the explicit
381    /// [`crate::vm::AsyncBuiltinCtx`] threaded by the dispatch loop.
382    pub fn register_async_builtin<F, Fut>(&mut self, name: &str, f: F)
383    where
384        F: Fn(crate::vm::AsyncBuiltinCtx, Vec<VmValue>) -> Fut + Send + Sync + 'static,
385        Fut: Future<Output = Result<VmValue, VmError>> + Send + 'static,
386    {
387        Arc::make_mut(&mut self.async_builtins).insert(
388            name.to_string(),
389            Arc::new(move |ctx, args| Box::pin(f(ctx, args))),
390        );
391        Arc::make_mut(&mut self.builtin_metadata).insert(
392            name.to_string(),
393            VmBuiltinMetadata::async_builtin(name.to_string()),
394        );
395        self.refresh_builtin_id(name);
396    }
397
398    /// Register a dynamically supplied async builtin with a complete typed
399    /// source exposure/effect contract.
400    pub fn register_async_builtin_with_contract<F, Fut>(
401        &mut self,
402        name: &str,
403        contract: harn_builtin_meta::BuiltinContract,
404        f: F,
405    ) where
406        F: Fn(crate::vm::AsyncBuiltinCtx, Vec<VmValue>) -> Fut + Send + Sync + 'static,
407        Fut: Future<Output = Result<VmValue, VmError>> + Send + 'static,
408    {
409        self.register_async_builtin_with_metadata(
410            VmBuiltinMetadata::async_builtin(name.to_string()).with_contract(contract),
411            f,
412        );
413    }
414
415    /// Register an async builtin function with discoverable metadata. The
416    /// handler receives the explicit [`crate::vm::AsyncBuiltinCtx`].
417    pub fn register_async_builtin_with_metadata<F, Fut>(
418        &mut self,
419        metadata: VmBuiltinMetadata,
420        f: F,
421    ) where
422        F: Fn(crate::vm::AsyncBuiltinCtx, Vec<VmValue>) -> Fut + Send + Sync + 'static,
423        Fut: Future<Output = Result<VmValue, VmError>> + Send + 'static,
424    {
425        let name = metadata.name().to_string();
426        Arc::make_mut(&mut self.async_builtins).insert(
427            name.clone(),
428            Arc::new(move |ctx, args| Box::pin(f(ctx, args))),
429        );
430        Arc::make_mut(&mut self.builtin_metadata)
431            .insert(name.clone(), metadata.with_kind(VmBuiltinKind::Async));
432        self.refresh_builtin_id(&name);
433    }
434
435    /// Install a host implementation behind a typed capability method.
436    ///
437    /// The implementation is deliberately absent from the ordinary builtin
438    /// maps, so registering it cannot create an ambient source-level name.
439    /// The parser-visible signature/effect contract must independently name
440    /// the same `(capability, method)` pair in the builtin manifest.
441    pub fn register_capability_method<F>(
442        &mut self,
443        capability: harn_builtin_meta::CapabilityId,
444        method: &str,
445        f: F,
446    ) where
447        F: Fn(&[VmValue], &mut String) -> Result<VmValue, VmError> + Send + Sync + 'static,
448    {
449        self.insert_capability_method(capability, method, VmBuiltinDispatch::Sync(Arc::new(f)));
450    }
451
452    /// Async counterpart of [`Self::register_capability_method`].
453    pub fn register_async_capability_method<F, Fut>(
454        &mut self,
455        capability: harn_builtin_meta::CapabilityId,
456        method: &str,
457        f: F,
458    ) where
459        F: Fn(crate::vm::AsyncBuiltinCtx, Vec<VmValue>) -> Fut + Send + Sync + 'static,
460        Fut: Future<Output = Result<VmValue, VmError>> + Send + 'static,
461    {
462        self.insert_capability_method(
463            capability,
464            method,
465            VmBuiltinDispatch::Async(Arc::new(move |ctx, args| Box::pin(f(ctx, args)))),
466        );
467    }
468
469    fn insert_capability_method(
470        &mut self,
471        capability: harn_builtin_meta::CapabilityId,
472        method: &str,
473        dispatch: VmBuiltinDispatch,
474    ) {
475        let replaced = Arc::make_mut(&mut self.capability_methods)
476            .entry(capability)
477            .or_default()
478            .insert(method.to_string(), dispatch);
479        assert!(
480            replaced.is_none(),
481            "capability method harness.{}.{} registered twice",
482            capability.field_name(),
483            method
484        );
485    }
486
487    pub(crate) fn registered_builtin_id(&self, name: &str) -> Option<BuiltinId> {
488        let id = BuiltinId::from_name(name);
489        if self
490            .builtins_by_id
491            .get(&id)
492            .is_some_and(|entry| entry.name.as_ref() == name)
493        {
494            Some(id)
495        } else {
496            None
497        }
498    }
499
500    /// Invoke a closure inline against the existing VM frame stack.
501    ///
502    /// Dispatch path for every callback-taking method on lists/dicts/sets
503    /// (`.map`, `.filter`, `.reduce`, `.each`, `.sort_by`, …) via
504    /// [`call_callable_value`]. The closure's frame is pushed onto
505    /// `self.frames` using the same machinery as `Op::Call`, and the
506    /// shared dispatch loop ([`Vm::drive_until_frame_depth`]) drains the
507    /// sub-execution back to the caller's depth.
508    ///
509    /// This avoids the per-invocation `Pin<Box<dyn Future>>` heap
510    /// allocation a recursive `async fn` would require — the recursion
511    /// cycle (closure → `.map` → callback → closure) is broken instead at
512    /// [`Vm::call_method`], which keeps a single boxed future per
513    /// method-call site rather than per callback element.
514    ///
515    /// Exception handlers are saved and cleared before the sub-execution
516    /// so an unhandled throw inside the body propagates as a Rust
517    /// `Result::Err` to the caller's dispatch loop. Iterators, deadlines,
518    /// and frames are scoped by `CallFrame::saved_iterator_depth` and the
519    /// per-frame deadline tags.
520    pub(crate) async fn call_closure(
521        &mut self,
522        closure: &VmClosure,
523        args: &[VmValue],
524    ) -> Result<VmValue, VmError> {
525        self.call_closure_args(closure, CallArgs::Slice(args)).await
526    }
527
528    pub(crate) async fn call_closure_args(
529        &mut self,
530        closure: &VmClosure,
531        args: CallArgs<'_>,
532    ) -> Result<VmValue, VmError> {
533        let saved_handlers = std::mem::take(&mut self.exception_handlers);
534        let active_context = (!crate::step_runtime::is_tracked_function(&closure.func.name))
535            .then(crate::step_runtime::suspend_active_context);
536
537        let target_frame_depth = self.frames.len();
538        let frame_result = self.push_closure_frame_args(closure, &args);
539        drop(args);
540        let result = match frame_result {
541            Ok(()) => self.drive_until_frame_depth(target_frame_depth).await,
542            Err(e) => Err(e),
543        };
544
545        self.exception_handlers = saved_handlers;
546        drop(active_context);
547
548        result
549    }
550
551    /// Invoke a value as a callable. Supports `VmValue::Closure` and
552    /// `VmValue::BuiltinRef`, so builtin names passed by reference (e.g.
553    /// `dict.rekeyed(snake_to_camel)`) dispatch through the same code path as
554    /// user-defined closures.
555    pub(crate) async fn call_callable_value(
556        &mut self,
557        callable: &VmValue,
558        args: &[VmValue],
559    ) -> Result<VmValue, VmError> {
560        self.call_callable_args(callable, CallArgs::Slice(args))
561            .await
562    }
563
564    pub(crate) async fn call_callable_owned(
565        &mut self,
566        callable: &VmValue,
567        args: Vec<VmValue>,
568    ) -> Result<VmValue, VmError> {
569        self.call_callable_args(callable, CallArgs::Owned(args))
570            .await
571    }
572
573    pub(crate) async fn call_callable_zero(
574        &mut self,
575        callable: &VmValue,
576    ) -> Result<VmValue, VmError> {
577        self.call_callable_args(callable, CallArgs::Empty).await
578    }
579
580    pub(crate) async fn call_callable_one(
581        &mut self,
582        callable: &VmValue,
583        arg: &VmValue,
584    ) -> Result<VmValue, VmError> {
585        self.call_callable_args(callable, CallArgs::One(arg)).await
586    }
587
588    pub(crate) async fn call_callable_two(
589        &mut self,
590        callable: &VmValue,
591        first: &VmValue,
592        second: &VmValue,
593    ) -> Result<VmValue, VmError> {
594        self.call_callable_args(callable, CallArgs::Two(first, second))
595            .await
596    }
597
598    pub(crate) async fn call_callable_args(
599        &mut self,
600        callable: &VmValue,
601        args: CallArgs<'_>,
602    ) -> Result<VmValue, VmError> {
603        match callable {
604            VmValue::Closure(closure) => self.call_closure_args(closure, args).await,
605            VmValue::Dict(registry) => {
606                let handler =
607                    crate::vm::tool_callable::require_single_harn_tool_handler(registry, || {
608                        "expected callable, got dict".to_string()
609                    })?;
610                self.call_closure_args(&handler, args).await
611            }
612            VmValue::BuiltinRef(name) => {
613                if !crate::autonomy::needs_async_side_effect_enforcement(name) {
614                    if let Some(result) = self.call_sync_builtin_by_ref_args(name, &args) {
615                        return result;
616                    }
617                }
618                self.call_named_builtin(name, args.into_vec()).await
619            }
620            VmValue::BuiltinRefId(r) => {
621                if let Some(result) =
622                    self.try_call_sync_builtin_id_or_name_args(Some(r.id), &r.name, &args)
623                {
624                    return result;
625                }
626                self.call_builtin_id_or_name(r.id, &r.name, args.into_vec())
627                    .await
628            }
629            other => Err(VmError::TypeError(format!(
630                "expected callable, got {}",
631                other.type_name()
632            ))),
633        }
634    }
635
636    fn call_sync_builtin_by_ref_args(
637        &mut self,
638        name: &str,
639        args: &CallArgs<'_>,
640    ) -> Option<Result<VmValue, VmError>> {
641        self.try_call_sync_builtin_id_or_name_args(None, name, args)
642    }
643
644    /// Returns true if `v` is callable via `call_callable_value`.
645    pub(crate) fn is_callable_value(v: &VmValue) -> bool {
646        matches!(
647            v,
648            VmValue::Closure(_) | VmValue::BuiltinRef(_) | VmValue::BuiltinRefId(_)
649        ) || crate::vm::tool_callable::is_single_harn_tool_registry_value(v)
650    }
651
652    /// Public wrapper for `call_closure`, used by the MCP server to invoke
653    /// tool handler closures from outside the VM execution loop.
654    pub async fn call_closure_pub(
655        &mut self,
656        closure: &VmClosure,
657        args: &[VmValue],
658    ) -> Result<VmValue, VmError> {
659        self.ensure_execution_available()?;
660        self.cancel_grace_instructions_remaining = None;
661        self.call_closure(closure, args).await
662    }
663
664    /// Resolve a named builtin: sync builtins → async builtins → bridge → error.
665    /// Used by Call, TailCall, and Pipe handlers to avoid duplicating this lookup.
666    pub(crate) async fn call_named_builtin(
667        &mut self,
668        name: &str,
669        args: Vec<VmValue>,
670    ) -> Result<VmValue, VmError> {
671        self.call_builtin_impl(name, args, None, true).await
672    }
673
674    /// Invoke a hidden builtin that implements an already-authorized Harness
675    /// method. The nominal capability contract is the policy/effect owner;
676    /// applying the legacy builtin contract again would duplicate receipts
677    /// and, for approval-gated effects, request human approval twice.
678    pub(in crate::vm) async fn call_capability_builtin(
679        &mut self,
680        name: &str,
681        args: Vec<VmValue>,
682    ) -> Result<VmValue, VmError> {
683        self.call_builtin_impl(name, args, None, false).await
684    }
685
686    /// Invoke a synchronous hidden builtin behind an already-authorized
687    /// Harness method without constructing the recursive async dispatcher.
688    ///
689    /// Capability dispatch has already applied autonomy, policy, and receipt
690    /// handling before reaching this seam. Calling the sync handler directly
691    /// therefore preserves one contract owner and keeps deeply nested
692    /// agent/tool execution from accumulating the much larger async builtin
693    /// frame for ordinary filesystem operations.
694    pub(in crate::vm) fn call_capability_sync_builtin(
695        &mut self,
696        name: &str,
697        args: &[VmValue],
698    ) -> Result<VmValue, VmError> {
699        if self.denied_builtins.contains(name) {
700            return Err(VmError::CategorizedError {
701                message: format!("Tool '{name}' is not permitted."),
702                category: ErrorCategory::ToolRejected,
703            });
704        }
705        let builtin = self
706            .builtins
707            .get(name)
708            .cloned()
709            .ok_or_else(|| VmError::UndefinedBuiltin(name.to_string()))?;
710        let _observe = Self::observe_builtin_call(name);
711        // The Harness method contract has already applied policy and recorded
712        // its typed effects. Only validate the hidden implementation's runtime
713        // argument shape here; `validate_sync_builtin_args` would reapply the
714        // obsolete ambient-builtin policy and can reject an already-approved
715        // capability call.
716        crate::typecheck::validate_builtin_call(name, args, None)?;
717        let _interrupt = self.sync_builtin_interrupt_guard();
718        builtin(args, &mut self.output)
719    }
720
721    pub(crate) async fn call_builtin_id_or_name(
722        &mut self,
723        id: BuiltinId,
724        name: &str,
725        args: Vec<VmValue>,
726    ) -> Result<VmValue, VmError> {
727        self.call_builtin_impl(name, args, Some(id), true).await
728    }
729
730    /// Install the thread-local [`crate::op_interrupt`] context for the
731    /// duration of a sync builtin call, so blocking builtins (subprocess
732    /// waits in particular) can observe scope cancellation and `deadline`
733    /// expiry that the async `tokio::select!` wrapper cannot deliver while
734    /// the op future is stuck inside a synchronous handler. Returns `None`
735    /// (no thread-local traffic) when nothing is armed.
736    pub(in crate::vm) fn sync_builtin_interrupt_guard(
737        &self,
738    ) -> Option<crate::op_interrupt::OpInterruptGuard> {
739        // Mirror `execution.rs::next_deadline`: innermost scope deadline,
740        // tightened by the interrupt-handler deadline when that is sooner.
741        let scope_deadline = self.deadlines.last().map(|(deadline, _)| *deadline);
742        let deadline = match (scope_deadline, self.interrupt_handler_deadline) {
743            (Some(scope), Some(interrupt)) => Some(scope.min(interrupt)),
744            (scope, interrupt) => scope.or(interrupt),
745        };
746        if self.cancel_token.is_none() && deadline.is_none() {
747            return None;
748        }
749        Some(crate::op_interrupt::install(
750            self.cancel_token.clone(),
751            deadline,
752        ))
753    }
754
755    pub(crate) fn try_call_sync_builtin_id_or_name_args(
756        &mut self,
757        direct_id: Option<BuiltinId>,
758        name: &str,
759        args: &CallArgs<'_>,
760    ) -> Option<Result<VmValue, VmError>> {
761        if self.denied_builtins.contains(name) {
762            return Some(Err(VmError::CategorizedError {
763                message: format!("Tool '{name}' is not permitted."),
764                category: ErrorCategory::ToolRejected,
765            }));
766        }
767        let resolved = match self.resolve_sync_builtin_id_or_name(direct_id, name)? {
768            Ok(resolved) => resolved,
769            Err(error) => return Some(Err(error)),
770        };
771        let _observe = Self::observe_builtin_call(name);
772        if let Err(error) = args.with_slice(|slice| {
773            self.validate_sync_builtin_args(name, slice, resolved.recorded_effects)
774        }) {
775            return Some(Err(error));
776        }
777
778        let _interrupt = self.sync_builtin_interrupt_guard();
779        Some(args.with_slice(|slice| (resolved.handler)(slice, &mut self.output)))
780    }
781
782    pub(crate) fn try_call_sync_builtin_id_or_name_from_stack_args(
783        &mut self,
784        direct_id: Option<BuiltinId>,
785        name: &str,
786        args_start: usize,
787    ) -> Option<Result<VmValue, VmError>> {
788        if self.denied_builtins.contains(name) {
789            return Some(Err(VmError::CategorizedError {
790                message: format!("Tool '{name}' is not permitted."),
791                category: ErrorCategory::ToolRejected,
792            }));
793        }
794        let resolved = match self.resolve_sync_builtin_id_or_name(direct_id, name)? {
795            Ok(resolved) => resolved,
796            Err(error) => return Some(Err(error)),
797        };
798        if args_start > self.stack.len() {
799            return Some(Err(VmError::Runtime(
800                "call argument stack underflow".to_string(),
801            )));
802        }
803
804        let _observe = Self::observe_builtin_call(name);
805        if let Err(error) = self.validate_sync_builtin_args(
806            name,
807            &self.stack[args_start..],
808            resolved.recorded_effects,
809        ) {
810            return Some(Err(error));
811        }
812
813        let _interrupt = self.sync_builtin_interrupt_guard();
814        Some((resolved.handler)(
815            &self.stack[args_start..],
816            &mut self.output,
817        ))
818    }
819
820    async fn call_builtin_impl(
821        &mut self,
822        name: &str,
823        args: Vec<VmValue>,
824        direct_id: Option<BuiltinId>,
825        enforce_contract: bool,
826    ) -> Result<VmValue, VmError> {
827        let _observe = Self::observe_builtin_call(name);
828
829        // Sandbox check: deny builtins blocked by --deny/--allow flags.
830        if self.denied_builtins.contains(name) {
831            return Err(VmError::CategorizedError {
832                message: format!("Tool '{name}' is not permitted."),
833                category: ErrorCategory::ToolRejected,
834            });
835        }
836        let autonomy =
837            if enforce_contract && crate::autonomy::needs_async_side_effect_enforcement(name) {
838                crate::autonomy::enforce_builtin_side_effect_boxed(name, &args).await?
839            } else {
840                None
841            };
842        if let Some(crate::autonomy::AutonomyDecision::Skip(value)) = autonomy {
843            return Ok(value);
844        }
845        if enforce_contract {
846            if !matches!(
847                autonomy,
848                Some(crate::autonomy::AutonomyDecision::AllowApproved)
849            ) {
850                crate::orchestration::enforce_current_policy_for_builtin(name, &args)?;
851            }
852            self.record_builtin_contract_effects(name, &args);
853        }
854        crate::typecheck::validate_builtin_call(name, &args, None)?;
855
856        if let Some(id) = direct_id {
857            if let Some(entry) = self.builtins_by_id.get(&id).cloned() {
858                if entry.name.as_ref() == name {
859                    return self.call_builtin_entry(name, entry.dispatch, args).await;
860                }
861            }
862        }
863
864        if let Some(builtin) = self.builtins.get(name).cloned() {
865            self.call_builtin_entry(name, VmBuiltinDispatch::Sync(builtin), args)
866                .await
867        } else if let Some(async_builtin) = self.async_builtins.get(name).cloned() {
868            self.call_builtin_entry(name, VmBuiltinDispatch::Async(async_builtin), args)
869                .await
870        } else if let Some(bridge) = &self.bridge {
871            if enforce_contract {
872                crate::orchestration::enforce_current_policy_for_bridge_builtin(name)?;
873            }
874            let args_json: Vec<serde_json::Value> =
875                args.iter().map(crate::llm::vm_value_to_json).collect();
876            let result = bridge
877                .call(
878                    "builtin_call",
879                    serde_json::json!({"name": name, "args": args_json}),
880                )
881                .await?;
882            Ok(crate::bridge::json_result_to_vm_value(&result))
883        } else {
884            let all_builtins = self
885                .builtins
886                .keys()
887                .chain(self.async_builtins.keys())
888                .map(|s| s.as_str());
889            if let Some(suggestion) = crate::value::closest_match(name, all_builtins) {
890                return Err(VmError::Runtime(format!(
891                    "Undefined builtin: {name} (did you mean `{suggestion}`?)"
892                )));
893            }
894            Err(VmError::UndefinedBuiltin(name.to_string()))
895        }
896    }
897
898    pub(in crate::vm) async fn call_builtin_entry(
899        &mut self,
900        name: &str,
901        dispatch: VmBuiltinDispatch,
902        args: Vec<VmValue>,
903    ) -> Result<VmValue, VmError> {
904        let result = match dispatch {
905            VmBuiltinDispatch::Sync(builtin) => {
906                let _interrupt = self.sync_builtin_interrupt_guard();
907                builtin(&args, &mut self.output)
908            }
909            VmBuiltinDispatch::Async(async_builtin) => {
910                // Bind a fresh child VM as the async-builtin context for the
911                // duration of this future, threading the explicit ctx handle
912                // into the handler. Drain any output VM-side closures
913                // forwarded into the ctx back to the parent.
914                let (result, captured) =
915                    crate::vm::run_async_builtin_with(self.child_vm_inline(), |ctx| {
916                        async_builtin(ctx, args)
917                    })
918                    .await;
919                if !captured.is_empty() {
920                    self.output.push_str(&captured);
921                }
922                result
923            }
924        }?;
925        if matches!(
926            name,
927            "sync_mutex_acquire"
928                | "sync_semaphore_acquire"
929                | "sync_gate_acquire"
930                | "sync_rwlock_acquire"
931        ) {
932            if let VmValue::SyncPermit(permit) = &result {
933                self.adopt_sync_permit_for_current_scope(permit.as_ref().clone());
934            }
935        }
936        Ok(result)
937    }
938}
939
940/// Build the discoverable [`VmBuiltinMetadata`] for one entry of a
941/// `#[harn_builtin]`-emitted `VmBuiltinDef`, threading the optional
942/// category / doc / signature_text fields without duplicating the chain
943/// across the Sync / Async dispatch arms in `register_builtin_def`.
944fn builtin_def_metadata(
945    def: &'static crate::stdlib::macros::VmBuiltinDef,
946    name: &'static str,
947    arity: VmBuiltinArity,
948    kind: VmBuiltinKind,
949) -> VmBuiltinMetadata {
950    let mut meta = match kind {
951        VmBuiltinKind::Sync => VmBuiltinMetadata::sync_static(name),
952        VmBuiltinKind::Async => VmBuiltinMetadata::async_static(name),
953    }
954    .arity(arity);
955    if let Some(category) = def.category {
956        meta = meta.category_static(category);
957    }
958    if let Some(doc) = def.doc {
959        meta = meta.doc_static(doc);
960    }
961    if let Some(sig_text) = def.signature_text {
962        meta = meta.signature_static(sig_text);
963    } else {
964        // Builtins declared via `sig_expr = …` (a canonical
965        // `harn_builtin_meta::signatures` const) carry no human-typed `sig`
966        // string, so render the parsed signature back through its `Display`
967        // impl. `Display` round-trips through the macro sig grammar (enforced
968        // by the signature-text drift test), so `harn explain` / LSP hover
969        // still surface an accurate, canonical signature.
970        meta = meta.signature_owned(format!("{}", def.sig));
971    }
972    meta.with_contract(def.contract)
973}
974
975/// Derive a [`VmBuiltinArity`] from a parsed [`BuiltinSignature`]. Required
976/// params count toward the floor; optional params and `has_rest` widen the
977/// ceiling. Returns `Variadic` for `(...args: any)`-shaped sigs that have
978/// no required params.
979fn arity_from_sig(sig: &harn_builtin_meta::BuiltinSignature) -> VmBuiltinArity {
980    let required = sig.params.iter().filter(|p| !p.optional).count();
981    let total = sig.params.len();
982    if sig.has_rest {
983        if required == 0 {
984            VmBuiltinArity::Variadic
985        } else {
986            VmBuiltinArity::Min(required)
987        }
988    } else if required == total {
989        VmBuiltinArity::Exact(total)
990    } else {
991        VmBuiltinArity::Range {
992            min: required,
993            max: total,
994        }
995    }
996}