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