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
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                Arc::make_mut(&mut self.builtins_by_id).remove(&id);
106                Arc::make_mut(&mut self.builtin_id_collisions).insert(id);
107                return;
108            }
109        }
110        Arc::make_mut(&mut self.builtins_by_id).insert(
111            id,
112            VmBuiltinEntry {
113                name: std::sync::Arc::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                Arc::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> + Send + Sync + 'static,
140    {
141        Arc::make_mut(&mut self.builtins).insert(name.to_string(), Arc::new(f));
142        Arc::make_mut(&mut self.builtin_metadata)
143            .insert(name.to_string(), VmBuiltinMetadata::sync(name.to_string()));
144        self.refresh_builtin_id(name);
145    }
146
147    /// Register a sync builtin function with discoverable metadata.
148    pub fn register_builtin_with_metadata<F>(&mut self, metadata: VmBuiltinMetadata, f: F)
149    where
150        F: Fn(&[VmValue], &mut String) -> Result<VmValue, VmError> + Send + Sync + 'static,
151    {
152        let name = metadata.name().to_string();
153        Arc::make_mut(&mut self.builtins).insert(name.clone(), Arc::new(f));
154        Arc::make_mut(&mut self.builtin_metadata)
155            .insert(name.clone(), metadata.with_kind(VmBuiltinKind::Sync));
156        self.refresh_builtin_id(&name);
157    }
158
159    /// Register a `VmBuiltinDef` (the shape emitted by `#[harn_builtin]`).
160    /// Registers the primary name plus each declared alias, sharing the
161    /// same handler. `runtime_only` defs skip the parser-side publish (the
162    /// vm-side registration still happens). `parser_only` defs skip the
163    /// vm-side registration entirely (handler is `None`).
164    pub fn register_builtin_def(&mut self, def: &'static crate::stdlib::macros::VmBuiltinDef) {
165        use crate::stdlib::macros::VmBuiltinHandler;
166        if def.parser_only {
167            return;
168        }
169        // Derive arity from the parsed `BuiltinSignature` so the discoverable
170        // metadata layer (harn explain, alignment-test metadata check) keeps
171        // parity with the pre-macro DSL builder.
172        let arity = arity_from_sig(&def.sig);
173        let names = std::iter::once(def.sig.name).chain(def.aliases.iter().copied());
174        for name in names {
175            match def.handler {
176                VmBuiltinHandler::Sync(f) => {
177                    let meta = builtin_def_metadata(def, name, arity, VmBuiltinKind::Sync);
178                    self.register_builtin_with_metadata(meta, f);
179                }
180                VmBuiltinHandler::Async(f) => {
181                    let meta = builtin_def_metadata(def, name, arity, VmBuiltinKind::Async);
182                    // Wrap the function pointer that already returns an
183                    // AsyncBuiltinFuture so register_async_builtin_with_metadata's
184                    // generic handler/future bounds are met.
185                    self.register_async_builtin_with_metadata(meta, f);
186                }
187                VmBuiltinHandler::None => {
188                    // Parser-only, but reached here despite parser_only=false.
189                    // This is a configuration bug.
190                    panic!(
191                        "VmBuiltinHandler::None for {name:?} without parser_only=true \
192                         on its BuiltinDef"
193                    );
194                }
195            }
196        }
197    }
198
199    /// Remove a sync builtin (so an async version can take precedence).
200    pub fn unregister_builtin(&mut self, name: &str) {
201        Arc::make_mut(&mut self.builtins).remove(name);
202        if self.async_builtins.contains_key(name) {
203            Arc::make_mut(&mut self.builtin_metadata).insert(
204                name.to_string(),
205                VmBuiltinMetadata::async_builtin(name.to_string()),
206            );
207        } else {
208            Arc::make_mut(&mut self.builtin_metadata).remove(name);
209        }
210        self.refresh_builtin_id(name);
211    }
212
213    /// Register an async builtin function. The handler receives the explicit
214    /// [`crate::vm::AsyncBuiltinCtx`] threaded by the dispatch loop.
215    pub fn register_async_builtin<F, Fut>(&mut self, name: &str, f: F)
216    where
217        F: Fn(crate::vm::AsyncBuiltinCtx, Vec<VmValue>) -> Fut + Send + Sync + 'static,
218        Fut: Future<Output = Result<VmValue, VmError>> + Send + 'static,
219    {
220        Arc::make_mut(&mut self.async_builtins).insert(
221            name.to_string(),
222            Arc::new(move |ctx, args| Box::pin(f(ctx, args))),
223        );
224        Arc::make_mut(&mut self.builtin_metadata).insert(
225            name.to_string(),
226            VmBuiltinMetadata::async_builtin(name.to_string()),
227        );
228        self.refresh_builtin_id(name);
229    }
230
231    /// Register an async builtin function with discoverable metadata. The
232    /// handler receives the explicit [`crate::vm::AsyncBuiltinCtx`].
233    pub fn register_async_builtin_with_metadata<F, Fut>(
234        &mut self,
235        metadata: VmBuiltinMetadata,
236        f: F,
237    ) where
238        F: Fn(crate::vm::AsyncBuiltinCtx, Vec<VmValue>) -> Fut + Send + Sync + 'static,
239        Fut: Future<Output = Result<VmValue, VmError>> + Send + 'static,
240    {
241        let name = metadata.name().to_string();
242        Arc::make_mut(&mut self.async_builtins).insert(
243            name.clone(),
244            Arc::new(move |ctx, args| Box::pin(f(ctx, args))),
245        );
246        Arc::make_mut(&mut self.builtin_metadata)
247            .insert(name.clone(), metadata.with_kind(VmBuiltinKind::Async));
248        self.refresh_builtin_id(&name);
249    }
250
251    pub(crate) fn registered_builtin_id(&self, name: &str) -> Option<BuiltinId> {
252        let id = BuiltinId::from_name(name);
253        if self
254            .builtins_by_id
255            .get(&id)
256            .is_some_and(|entry| entry.name.as_ref() == name)
257        {
258            Some(id)
259        } else {
260            None
261        }
262    }
263
264    /// Invoke a closure inline against the existing VM frame stack.
265    ///
266    /// Dispatch path for every callback-taking method on lists/dicts/sets
267    /// (`.map`, `.filter`, `.reduce`, `.each`, `.sort_by`, …) via
268    /// [`call_callable_value`]. The closure's frame is pushed onto
269    /// `self.frames` using the same machinery as `Op::Call`, and the
270    /// shared dispatch loop ([`Vm::drive_until_frame_depth`]) drains the
271    /// sub-execution back to the caller's depth.
272    ///
273    /// This avoids the per-invocation `Pin<Box<dyn Future>>` heap
274    /// allocation a recursive `async fn` would require — the recursion
275    /// cycle (closure → `.map` → callback → closure) is broken instead at
276    /// [`Vm::call_method`], which keeps a single boxed future per
277    /// method-call site rather than per callback element.
278    ///
279    /// Exception handlers are saved and cleared before the sub-execution
280    /// so an unhandled throw inside the body propagates as a Rust
281    /// `Result::Err` to the caller's dispatch loop. Iterators, deadlines,
282    /// and frames are scoped by `CallFrame::saved_iterator_depth` and the
283    /// per-frame deadline tags.
284    pub(crate) async fn call_closure(
285        &mut self,
286        closure: &VmClosure,
287        args: &[VmValue],
288    ) -> Result<VmValue, VmError> {
289        self.call_closure_args(closure, CallArgs::Slice(args)).await
290    }
291
292    pub(crate) async fn call_closure_args(
293        &mut self,
294        closure: &VmClosure,
295        args: CallArgs<'_>,
296    ) -> Result<VmValue, VmError> {
297        let saved_handlers = std::mem::take(&mut self.exception_handlers);
298        let active_context = (!crate::step_runtime::is_tracked_function(&closure.func.name))
299            .then(crate::step_runtime::take_active_context);
300
301        let target_frame_depth = self.frames.len();
302        let frame_result = self.push_closure_frame_args(closure, &args);
303        drop(args);
304        let result = match frame_result {
305            Ok(()) => self.drive_until_frame_depth(target_frame_depth).await,
306            Err(e) => Err(e),
307        };
308
309        self.exception_handlers = saved_handlers;
310        if let Some(ctx) = active_context {
311            crate::step_runtime::restore_active_context(ctx);
312        }
313
314        result
315    }
316
317    /// Invoke a value as a callable. Supports `VmValue::Closure` and
318    /// `VmValue::BuiltinRef`, so builtin names passed by reference (e.g.
319    /// `dict.rekey(snake_to_camel)`) dispatch through the same code path as
320    /// user-defined closures.
321    pub(crate) async fn call_callable_value(
322        &mut self,
323        callable: &VmValue,
324        args: &[VmValue],
325    ) -> Result<VmValue, VmError> {
326        self.call_callable_args(callable, CallArgs::Slice(args))
327            .await
328    }
329
330    pub(crate) async fn call_callable_owned(
331        &mut self,
332        callable: &VmValue,
333        args: Vec<VmValue>,
334    ) -> Result<VmValue, VmError> {
335        self.call_callable_args(callable, CallArgs::Owned(args))
336            .await
337    }
338
339    pub(crate) async fn call_callable_zero(
340        &mut self,
341        callable: &VmValue,
342    ) -> Result<VmValue, VmError> {
343        self.call_callable_args(callable, CallArgs::Empty).await
344    }
345
346    pub(crate) async fn call_callable_one(
347        &mut self,
348        callable: &VmValue,
349        arg: &VmValue,
350    ) -> Result<VmValue, VmError> {
351        self.call_callable_args(callable, CallArgs::One(arg)).await
352    }
353
354    pub(crate) async fn call_callable_two(
355        &mut self,
356        callable: &VmValue,
357        first: &VmValue,
358        second: &VmValue,
359    ) -> Result<VmValue, VmError> {
360        self.call_callable_args(callable, CallArgs::Two(first, second))
361            .await
362    }
363
364    pub(crate) async fn call_callable_args(
365        &mut self,
366        callable: &VmValue,
367        args: CallArgs<'_>,
368    ) -> Result<VmValue, VmError> {
369        match callable {
370            VmValue::Closure(closure) => self.call_closure_args(closure, args).await,
371            VmValue::Dict(registry) => {
372                let handler =
373                    crate::vm::tool_callable::require_single_harn_tool_handler(registry, || {
374                        "expected callable, got dict".to_string()
375                    })?;
376                self.call_closure_args(&handler, args).await
377            }
378            VmValue::BuiltinRef(name) => {
379                if !crate::autonomy::needs_async_side_effect_enforcement(name) {
380                    if let Some(result) = self.call_sync_builtin_by_ref_args(name, &args) {
381                        return result;
382                    }
383                }
384                self.call_named_builtin(name, args.into_vec()).await
385            }
386            VmValue::BuiltinRefId(r) => {
387                if let Some(result) =
388                    self.try_call_sync_builtin_id_or_name_args(Some(r.id), &r.name, &args)
389                {
390                    return result;
391                }
392                self.call_builtin_id_or_name(r.id, &r.name, args.into_vec())
393                    .await
394            }
395            other => Err(VmError::TypeError(format!(
396                "expected callable, got {}",
397                other.type_name()
398            ))),
399        }
400    }
401
402    fn call_sync_builtin_by_ref_args(
403        &mut self,
404        name: &str,
405        args: &CallArgs<'_>,
406    ) -> Option<Result<VmValue, VmError>> {
407        self.try_call_sync_builtin_id_or_name_args(None, name, args)
408    }
409
410    /// Returns true if `v` is callable via `call_callable_value`.
411    pub(crate) fn is_callable_value(v: &VmValue) -> bool {
412        matches!(
413            v,
414            VmValue::Closure(_) | VmValue::BuiltinRef(_) | VmValue::BuiltinRefId(_)
415        ) || crate::vm::tool_callable::is_single_harn_tool_registry_value(v)
416    }
417
418    /// Public wrapper for `call_closure`, used by the MCP server to invoke
419    /// tool handler closures from outside the VM execution loop.
420    pub async fn call_closure_pub(
421        &mut self,
422        closure: &VmClosure,
423        args: &[VmValue],
424    ) -> Result<VmValue, VmError> {
425        self.ensure_execution_available()?;
426        self.cancel_grace_instructions_remaining = None;
427        self.call_closure(closure, args).await
428    }
429
430    /// Resolve a named builtin: sync builtins → async builtins → bridge → error.
431    /// Used by Call, TailCall, and Pipe handlers to avoid duplicating this lookup.
432    pub(crate) async fn call_named_builtin(
433        &mut self,
434        name: &str,
435        args: Vec<VmValue>,
436    ) -> Result<VmValue, VmError> {
437        self.call_builtin_impl(name, args, None).await
438    }
439
440    pub(crate) async fn call_builtin_id_or_name(
441        &mut self,
442        id: BuiltinId,
443        name: &str,
444        args: Vec<VmValue>,
445    ) -> Result<VmValue, VmError> {
446        self.call_builtin_impl(name, args, Some(id)).await
447    }
448
449    /// Install the thread-local [`crate::op_interrupt`] context for the
450    /// duration of a sync builtin call, so blocking builtins (subprocess
451    /// waits in particular) can observe scope cancellation and `deadline`
452    /// expiry that the async `tokio::select!` wrapper cannot deliver while
453    /// the op future is stuck inside a synchronous handler. Returns `None`
454    /// (no thread-local traffic) when nothing is armed.
455    pub(in crate::vm) fn sync_builtin_interrupt_guard(
456        &self,
457    ) -> Option<crate::op_interrupt::OpInterruptGuard> {
458        // Mirror `execution.rs::next_deadline`: innermost scope deadline,
459        // tightened by the interrupt-handler deadline when that is sooner.
460        let scope_deadline = self.deadlines.last().map(|(deadline, _)| *deadline);
461        let deadline = match (scope_deadline, self.interrupt_handler_deadline) {
462            (Some(scope), Some(interrupt)) => Some(scope.min(interrupt)),
463            (scope, interrupt) => scope.or(interrupt),
464        };
465        if self.cancel_token.is_none() && deadline.is_none() {
466            return None;
467        }
468        Some(crate::op_interrupt::install(
469            self.cancel_token.clone(),
470            deadline,
471        ))
472    }
473
474    pub(crate) fn try_call_sync_builtin_id_or_name_args(
475        &mut self,
476        direct_id: Option<BuiltinId>,
477        name: &str,
478        args: &CallArgs<'_>,
479    ) -> Option<Result<VmValue, VmError>> {
480        if self.denied_builtins.contains(name) {
481            return Some(Err(VmError::CategorizedError {
482                message: format!("Tool '{name}' is not permitted."),
483                category: ErrorCategory::ToolRejected,
484            }));
485        }
486        let builtin = match self.resolve_sync_builtin_id_or_name(direct_id, name)? {
487            Ok(builtin) => builtin,
488            Err(error) => return Some(Err(error)),
489        };
490        let _span =
491            Self::builtin_span_kind(name).map(|kind| ScopeSpan::new(kind, name.to_string()));
492        if let Err(error) = args.with_slice(|slice| self.validate_sync_builtin_args(name, slice)) {
493            return Some(Err(error));
494        }
495
496        let _interrupt = self.sync_builtin_interrupt_guard();
497        Some(args.with_slice(|slice| builtin(slice, &mut self.output)))
498    }
499
500    pub(crate) fn try_call_sync_builtin_id_or_name_from_stack_args(
501        &mut self,
502        direct_id: Option<BuiltinId>,
503        name: &str,
504        args_start: usize,
505    ) -> Option<Result<VmValue, VmError>> {
506        if self.denied_builtins.contains(name) {
507            return Some(Err(VmError::CategorizedError {
508                message: format!("Tool '{name}' is not permitted."),
509                category: ErrorCategory::ToolRejected,
510            }));
511        }
512        let builtin = match self.resolve_sync_builtin_id_or_name(direct_id, name)? {
513            Ok(builtin) => builtin,
514            Err(error) => return Some(Err(error)),
515        };
516        if args_start > self.stack.len() {
517            return Some(Err(VmError::Runtime(
518                "call argument stack underflow".to_string(),
519            )));
520        }
521
522        let _span =
523            Self::builtin_span_kind(name).map(|kind| ScopeSpan::new(kind, name.to_string()));
524        if let Err(error) = self.validate_sync_builtin_args(name, &self.stack[args_start..]) {
525            return Some(Err(error));
526        }
527
528        let _interrupt = self.sync_builtin_interrupt_guard();
529        Some(builtin(&self.stack[args_start..], &mut self.output))
530    }
531
532    async fn call_builtin_impl(
533        &mut self,
534        name: &str,
535        args: Vec<VmValue>,
536        direct_id: Option<BuiltinId>,
537    ) -> Result<VmValue, VmError> {
538        // Auto-trace LLM calls and tool calls.
539        let _span =
540            Self::builtin_span_kind(name).map(|kind| ScopeSpan::new(kind, name.to_string()));
541
542        // Sandbox check: deny builtins blocked by --deny/--allow flags.
543        if self.denied_builtins.contains(name) {
544            return Err(VmError::CategorizedError {
545                message: format!("Tool '{name}' is not permitted."),
546                category: ErrorCategory::ToolRejected,
547            });
548        }
549        let autonomy = if crate::autonomy::needs_async_side_effect_enforcement(name) {
550            crate::autonomy::enforce_builtin_side_effect_boxed(name, &args).await?
551        } else {
552            None
553        };
554        if let Some(crate::autonomy::AutonomyDecision::Skip(value)) = autonomy {
555            return Ok(value);
556        }
557        if !matches!(
558            autonomy,
559            Some(crate::autonomy::AutonomyDecision::AllowApproved)
560        ) {
561            crate::orchestration::enforce_current_policy_for_builtin(name, &args)?;
562        }
563        crate::typecheck::validate_builtin_call(name, &args, None)?;
564
565        if let Some(result) =
566            crate::runtime_context::dispatch_runtime_context_builtin(self, name, &args)
567        {
568            return result;
569        }
570
571        if let Some(id) = direct_id {
572            if let Some(entry) = self.builtins_by_id.get(&id).cloned() {
573                if entry.name.as_ref() == name {
574                    return self.call_builtin_entry(name, entry.dispatch, args).await;
575                }
576            }
577        }
578
579        if let Some(builtin) = self.builtins.get(name).cloned() {
580            self.call_builtin_entry(name, VmBuiltinDispatch::Sync(builtin), args)
581                .await
582        } else if let Some(async_builtin) = self.async_builtins.get(name).cloned() {
583            self.call_builtin_entry(name, VmBuiltinDispatch::Async(async_builtin), args)
584                .await
585        } else if let Some(bridge) = &self.bridge {
586            crate::orchestration::enforce_current_policy_for_bridge_builtin(name)?;
587            let args_json: Vec<serde_json::Value> =
588                args.iter().map(crate::llm::vm_value_to_json).collect();
589            let result = bridge
590                .call(
591                    "builtin_call",
592                    serde_json::json!({"name": name, "args": args_json}),
593                )
594                .await?;
595            Ok(crate::bridge::json_result_to_vm_value(&result))
596        } else {
597            let all_builtins = self
598                .builtins
599                .keys()
600                .chain(self.async_builtins.keys())
601                .map(|s| s.as_str());
602            if let Some(suggestion) = crate::value::closest_match(name, all_builtins) {
603                return Err(VmError::Runtime(format!(
604                    "Undefined builtin: {name} (did you mean `{suggestion}`?)"
605                )));
606            }
607            Err(VmError::UndefinedBuiltin(name.to_string()))
608        }
609    }
610
611    async fn call_builtin_entry(
612        &mut self,
613        name: &str,
614        dispatch: VmBuiltinDispatch,
615        args: Vec<VmValue>,
616    ) -> Result<VmValue, VmError> {
617        let result = match dispatch {
618            VmBuiltinDispatch::Sync(builtin) => {
619                let _interrupt = self.sync_builtin_interrupt_guard();
620                builtin(&args, &mut self.output)
621            }
622            VmBuiltinDispatch::Async(async_builtin) => {
623                // Bind a fresh child VM as the async-builtin context for the
624                // duration of this future, threading the explicit ctx handle
625                // into the handler. Drain any output VM-side closures
626                // forwarded into the ctx back to the parent.
627                let (result, captured) =
628                    crate::vm::run_async_builtin_with(self.child_vm_inline(), |ctx| {
629                        async_builtin(ctx, args)
630                    })
631                    .await;
632                if !captured.is_empty() {
633                    self.output.push_str(&captured);
634                }
635                result
636            }
637        }?;
638        if matches!(
639            name,
640            "sync_mutex_acquire"
641                | "sync_semaphore_acquire"
642                | "sync_gate_acquire"
643                | "sync_rwlock_acquire"
644        ) {
645            if let VmValue::SyncPermit(permit) = &result {
646                self.adopt_sync_permit_for_current_scope(permit.as_ref().clone());
647            }
648        }
649        Ok(result)
650    }
651}
652
653/// Build the discoverable [`VmBuiltinMetadata`] for one entry of a
654/// `#[harn_builtin]`-emitted `VmBuiltinDef`, threading the optional
655/// category / doc / signature_text fields without duplicating the chain
656/// across the Sync / Async dispatch arms in `register_builtin_def`.
657fn builtin_def_metadata(
658    def: &'static crate::stdlib::macros::VmBuiltinDef,
659    name: &'static str,
660    arity: VmBuiltinArity,
661    kind: VmBuiltinKind,
662) -> VmBuiltinMetadata {
663    let mut meta = match kind {
664        VmBuiltinKind::Sync => VmBuiltinMetadata::sync_static(name),
665        VmBuiltinKind::Async => VmBuiltinMetadata::async_static(name),
666    }
667    .arity(arity);
668    if let Some(category) = def.category {
669        meta = meta.category_static(category);
670    }
671    if let Some(doc) = def.doc {
672        meta = meta.doc_static(doc);
673    }
674    if let Some(sig_text) = def.signature_text {
675        meta = meta.signature_static(sig_text);
676    } else {
677        // Builtins declared via `sig_expr = …` (a canonical
678        // `harn_builtin_meta::signatures` const) carry no human-typed `sig`
679        // string, so render the parsed signature back through its `Display`
680        // impl. `Display` round-trips through the macro sig grammar (enforced
681        // by the signature-text drift test), so `harn explain` / LSP hover
682        // still surface an accurate, canonical signature.
683        meta = meta.signature_owned(format!("{}", def.sig));
684    }
685    meta
686}
687
688/// Derive a [`VmBuiltinArity`] from a parsed [`BuiltinSignature`]. Required
689/// params count toward the floor; optional params and `has_rest` widen the
690/// ceiling. Returns `Variadic` for `(...args: any)`-shaped sigs that have
691/// no required params.
692fn arity_from_sig(sig: &harn_builtin_meta::BuiltinSignature) -> VmBuiltinArity {
693    let required = sig.params.iter().filter(|p| !p.optional).count();
694    let total = sig.params.len();
695    if sig.has_rest {
696        if required == 0 {
697            VmBuiltinArity::Variadic
698        } else {
699            VmBuiltinArity::Min(required)
700        }
701    } else if required == total {
702        VmBuiltinArity::Exact(total)
703    } else {
704        VmBuiltinArity::Range {
705            min: required,
706            max: total,
707        }
708    }
709}