monty 0.0.19-beta.2

A sandboxed, snapshotable Python interpreter written in Rust.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
//! Signature representation and argument binding for user-defined *Python*
//! functions (`def` / `lambda`).
//!
//! This module handles Python function signatures including all parameter types:
//! positional-only, positional-or-keyword, *args, keyword-only, and **kwargs.
//! It also handles default values and the argument binding algorithm.
//!
//! Native (Rust-implemented) callables are bound by the sibling
//! [`bind_native`](super::bind_native) module instead. The two binders are
//! deliberately separate (different param representations, outputs, and
//! error families), but [`Signature::bind`] MUST stay behaviourally
//! identical to `bind_native`'s `ErrorFamily::Def` path — same
//! kwargs-before-overflow ordering, same wording, same `(and N keyword-only
//! argument(s))` counting. When changing `def` semantics in either file,
//! change both; coverage lives in `test_cases/function__arity_defaults.py`
//! and `test_cases/args__macro_errors.py`.

use crate::{
    args::{ArgPosIter, ArgValues},
    bytecode::VM,
    defer_drop_mut,
    exception_private::{ExcType, RunResult, SimpleException},
    expressions::Identifier,
    heap::{HeapData, HeapGuard},
    heap_traits::DropWithHeap,
    intern::{Interns, StringId},
    resource::ResourceTracker,
    types::{Dict, allocate_tuple},
    value::Value,
};

/// Represents a Python function signature with all parameter types.
///
/// A complete Python signature can include:
/// - Positional-only parameters (before `/`)
/// - Positional-or-keyword parameters (regular parameters)
/// - Variable positional parameter (`*args`)
/// - Keyword-only parameters (after `*` or `*args`)
/// - Variable keyword parameter (`**kwargs`)
///
/// # Default Values
///
/// Default values are tracked by count per parameter group. The `*_defaults_count` fields
/// indicate how many parameters (from the end of each group) have defaults. For example,
/// if `args = [a, b, c]` and `arg_defaults_count = 2`, then `b` and `c` have defaults.
///
/// Note: The actual default Values are evaluated at function definition time and stored
/// separately (in the heap as part of the function/closure object). This struct only
/// tracks the structure, not the values themselves.
///
/// # Namespace Layout
///
/// Parameters are laid out in the namespace in this order:
/// ```text
/// [pos_args][args][*args_slot?][kwargs][**kwargs_slot?]
/// ```
/// The `*args` slot is only present if `var_args` is Some.
/// The `**kwargs` slot is only present if `var_kwargs` is Some.
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub(crate) struct Signature {
    /// Positional-only parameters, e.g. `a, b` in `def f(a, b, /): ...`
    ///
    /// These can only be passed by position, not by keyword.
    pos_args: Option<Vec<StringId>>,

    /// Number of positional-only parameters with defaults (from the end).
    pos_defaults_count: usize,

    /// Positional-or-keyword parameters, e.g. `a, b` in `def f(a, b): ...`
    ///
    /// These can be passed either by position or by keyword.
    args: Option<Vec<StringId>>,

    /// Number of positional-or-keyword parameters with defaults (from the end).
    arg_defaults_count: usize,

    /// Variable positional parameter name, e.g. `args` in `def f(*args): ...`
    ///
    /// Collects excess positional arguments into a tuple.
    var_args: Option<StringId>,

    /// Keyword-only parameters, e.g. `c` in `def f(*, c): ...` or `def f(*args, c): ...`
    ///
    /// These can only be passed by keyword, not by position.
    kwargs: Option<Vec<StringId>>,

    /// Mapping from each keyword-only parameter to its default index (if any).
    ///
    /// Each entry corresponds to the same index in `kwargs`. A value of `Some(i)`
    /// points into the kwarg section of the defaults array, while `None` means
    /// the parameter is required.
    kwarg_default_map: Option<Vec<Option<usize>>>,

    /// Variable keyword parameter name, e.g. `kwargs` in `def f(**kwargs): ...`
    ///
    /// Collects excess keyword arguments into a dict.
    var_kwargs: Option<StringId>,

    /// How simple the signature is, used for fast path when binding
    bind_mode: BindMode,
}

#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
enum BindMode {
    /// If this is a simple signature (no defaults, no *args/**kwargs).
    ///
    /// Simple signatures can use a fast path for argument binding that avoids
    /// the full binding algorithm overhead. A simple signature has:
    /// - No positional-only parameters
    /// - No defaults for any parameters
    /// - No *args or **kwargs
    /// - No keyword-only parameters
    #[default]
    Simple,
    /// If this signature has only positional-or-keyword params with defaults.
    ///
    /// This identifies the common pattern `def f(a, b=1, c=2)` where:
    /// - No positional-only parameters
    /// - No *args or **kwargs
    /// - No keyword-only parameters
    /// - Has some default values
    ///
    /// These signatures can use a simplified binding that just fills positional
    /// args and applies defaults without the full algorithm overhead.
    SimpleWithDefaults,
    Complex,
}

impl Signature {
    /// Creates a full signature with all parameter types.
    ///
    /// # Arguments
    /// * `pos_args` - Positional-only parameter names
    /// * `pos_defaults_count` - Number of pos_args with defaults (from end)
    /// * `args` - Positional-or-keyword parameter names
    /// * `arg_defaults_count` - Number of args with defaults (from end)
    /// * `var_args` - Variable positional parameter name (*args)
    /// * `kwargs` - Keyword-only parameter names
    /// * `kwarg_default_map` - Mapping of kw-only parameters to default indices
    /// * `var_kwargs` - Variable keyword parameter name (**kwargs)
    #[expect(clippy::too_many_arguments)]
    pub fn new(
        pos_args: Vec<StringId>,
        pos_defaults_count: usize,
        args: Vec<StringId>,
        arg_defaults_count: usize,
        var_args: Option<StringId>,
        kwargs: Vec<StringId>,
        kwarg_default_map: Vec<Option<usize>>,
        var_kwargs: Option<StringId>,
    ) -> Self {
        let pos_args = if pos_args.is_empty() { None } else { Some(pos_args) };
        let has_kwonly = !kwargs.is_empty();
        let kwargs = if has_kwonly { Some(kwargs) } else { None };

        let bind_mode = if pos_args.is_none()
            && pos_defaults_count == 0
            && arg_defaults_count == 0
            && var_args.is_none()
            && kwargs.is_none()
            && var_kwargs.is_none()
        {
            BindMode::Simple
        } else if pos_args.is_none()
            && var_args.is_none()
            && kwargs.is_none()
            && var_kwargs.is_none()
            && arg_defaults_count > 0
        {
            BindMode::SimpleWithDefaults
        } else {
            BindMode::Complex
        };

        Self {
            pos_args,
            pos_defaults_count,
            args: if args.is_empty() { None } else { Some(args) },
            arg_defaults_count,
            var_args,
            kwargs,
            kwarg_default_map: if has_kwonly { Some(kwarg_default_map) } else { None },
            var_kwargs,
            bind_mode,
        }
    }

    /// Binds arguments to parameters according to Python's calling conventions.
    ///
    /// This implements the full argument binding algorithm:
    /// 1. Bind positional args to pos_args, then args (in order)
    /// 2. Bind keyword args to args and kwargs (NOT pos_args - positional-only)
    /// 3. Collect excess positional args into *args tuple
    /// 4. Collect excess keyword args into **kwargs dict
    /// 5. Apply defaults for missing parameters
    ///
    /// Returns a Vec<Value> ready to be injected into the namespace, laid out as:
    /// `[pos_args][args][*args_slot?][kwargs][**kwargs_slot?]`
    ///
    /// # Arguments
    /// * `args` - The arguments from the call site
    /// * `defaults` - Evaluated default values (layout: pos_defaults, arg_defaults, kwarg_defaults)
    /// * `heap` - The heap for allocating *args tuple and **kwargs dict
    /// * `interns` - For looking up parameter names in error messages
    /// * `func_name` - Function name for error messages
    /// * `namespace` - The namespace to populate with bound arguments. This is mutated in place and will need to be cleaned up on error.
    ///
    /// # Errors
    /// Returns an error if:
    /// - Too few or too many positional arguments
    /// - Missing required keyword-only arguments
    /// - Unexpected keyword argument
    /// - Positional-only parameter passed as keyword
    /// - Same argument passed both positionally and by keyword
    pub fn bind(
        &self,
        args: ArgValues,
        defaults: &[Value],
        vm: &mut VM<'_, impl ResourceTracker>,
        func_name: Identifier,
        namespace: &mut Vec<Value>,
    ) -> RunResult<()> {
        let (pos_iter, keyword_args) = args.into_parts();
        let n_kwargs = keyword_args.len();
        let namespace_base = namespace.len();

        // Fast path for simple signatures (no special params) and signatures
        // with only positional-or-keyword params and defaults, when no keyword
        // arguments are passed. Handled before building the kwargs iterator +
        // guard below, which are pure overhead in this common positional case.
        if n_kwargs == 0 && matches!(self.bind_mode, BindMode::Simple | BindMode::SimpleWithDefaults) {
            // No keyword arguments to bind; the empty container holds no refs.
            keyword_args.drop_with_heap(vm);
            match pos_iter {
                ArgPosIter::Empty => {}
                ArgPosIter::One(a) => {
                    namespace.push(a);
                }
                ArgPosIter::Two([a1, a2]) => {
                    namespace.push(a1);
                    namespace.push(a2);
                }
                ArgPosIter::Vec(args) => {
                    namespace.extend(args);
                }
            }

            let actual_count = namespace.len() - namespace_base;
            let param_count = self.param_count();

            if actual_count == param_count {
                // Exact match - no defaults needed
                return Ok(());
            } else if self.bind_mode == BindMode::SimpleWithDefaults {
                let required = self.required_positional_count();
                if actual_count >= required && actual_count < param_count {
                    // Apply defaults for remaining parameters
                    // Defaults are stored at the end of the defaults array for pos-or-kw params
                    let defaults_needed = param_count - actual_count;
                    let defaults_start = self.arg_defaults_count - defaults_needed;
                    for i in 0..defaults_needed {
                        namespace.push(defaults[defaults_start + i].clone_with_heap(vm));
                    }
                    return Ok(());
                }
            }

            return self.wrong_arg_count_error(actual_count, vm.interns, func_name);
        }

        // Full binding algorithm for complex signatures or kwargs. Convert
        // kwargs to an iterator and guard it so remaining items are cleaned up
        // on any error path.
        let keyword_args = keyword_args.into_iter();
        defer_drop_mut!(keyword_args, vm);

        // Extract interns before guards since HeapGuard borrows the full VM mutably
        // but we only need mutable access to the heap portion.
        let mut pos_iter_guard = HeapGuard::new(pos_iter, vm);
        let (pos_iter, vm) = pos_iter_guard.as_parts_mut();

        // Calculate how many positional params we have
        let pos_param_count = self.pos_arg_count();
        let arg_param_count = self.arg_count();
        let total_positional_params = pos_param_count + arg_param_count;

        // Too many positionals? Noted here but raised only *after* the keyword
        // loop: CPython binds keyword arguments first, so unexpected-kwarg and
        // multiple-values errors beat too-many-positional.
        let positional_count = pos_iter.len();
        let positional_overflow = self.max_positional_count().is_some_and(|max| positional_count > max);

        // Initialize result namespace with Undefined values for all slots
        // Layout: [pos_args][args][*args?][kwargs][**kwargs?]
        let var_args_offset = usize::from(self.var_args.is_some());
        namespace.resize_with(namespace.len() + self.total_slots(), || Value::Undefined);

        // Track which parameters have been bound (for duplicate detection)
        // Uses a u64 bitmap - supports up to 64 named parameters which is sufficient
        // for any reasonable Python function (Python itself has practical limits).
        // Note: this tracks only named params, not *args/**kwargs slots
        let mut bound_params: u64 = 0;

        // 1. Bind positional args to pos_args, then args

        // Bind to pos_args
        for (i, slot) in namespace[namespace_base..].iter_mut().enumerate().take(pos_param_count) {
            if let Some(val) = pos_iter.next() {
                *slot = val;
                bound_params |= 1 << i;
            }
        }

        // Bind to args
        for (i, slot) in namespace[namespace_base..]
            .iter_mut()
            .enumerate()
            .take(total_positional_params)
            .skip(pos_param_count)
        {
            if let Some(val) = pos_iter.next() {
                *slot = val;
                bound_params |= 1 << i;
            }
        }

        // 2. Collect excess positional args into *args tuple. Without `*args`
        // any excess (the deferred overflow) stays in `pos_iter`, drained by
        // its guard when the overflow error returns below.
        if self.var_args.is_some() {
            namespace[namespace_base + total_positional_params] = allocate_tuple(pos_iter.collect(), vm.heap)?;
        }

        // 3. Bind keyword args
        // Bind keywords to args and kwargs (not pos_args - those are positional-only)
        let mut excess_kwargs_guard = HeapGuard::new(self.var_kwargs.is_some().then(Dict::new), vm);
        let (excess_kwargs, vm) = excess_kwargs_guard.as_parts_mut();

        'kwargs: for (key, value) in keyword_args {
            // Guard key: dropped on most paths, consumed into **kwargs via into_parts().
            let mut key_guard = HeapGuard::new(key, vm);
            let (key, vm) = key_guard.as_parts_mut();

            // Guard value: consumed into namespace/excess_kwargs via into_inner(),
            // or dropped automatically on error paths.
            let mut value_guard = HeapGuard::new(value, vm);
            let vm = value_guard.heap();

            let Some(keyword_name) = key.as_either_str(vm.heap) else {
                return Err(ExcType::type_error("keywords must be strings"));
            };

            // Check if this keyword matches a positional-only param (error)
            if let Some(pos_args) = &self.pos_args
                && let Some(&param_id) = pos_args
                    .iter()
                    .find(|&&param_id| keyword_name.matches(param_id, vm.interns))
            {
                let func = vm.interns.get_str(func_name.name_id);
                let param = vm.interns.get_str(param_id);
                return Err(ExcType::type_error_positional_only(func, param));
            }

            // Try positional-or-keyword params
            if let Some(args) = &self.args {
                for (i, &param_id) in args.iter().enumerate() {
                    if keyword_name.matches(param_id, vm.interns) {
                        let ns_idx = pos_param_count + i;
                        if (bound_params & (1 << ns_idx)) != 0 {
                            let func = vm.interns.get_str(func_name.name_id);
                            let param = vm.interns.get_str(param_id);
                            return Err(ExcType::type_error_duplicate_arg(func, param));
                        }
                        let (value, _) = value_guard.into_parts();
                        namespace[namespace_base + ns_idx] = value;
                        bound_params |= 1 << ns_idx;
                        continue 'kwargs;
                    }
                }
            }

            // Try keyword-only params
            if let Some(kwargs) = &self.kwargs {
                for (i, &param_id) in kwargs.iter().enumerate() {
                    if keyword_name.matches(param_id, vm.interns) {
                        let ns_idx = total_positional_params + var_args_offset + i;
                        let bit_idx = total_positional_params + i;
                        if (bound_params & (1 << bit_idx)) != 0 {
                            let func = vm.interns.get_str(func_name.name_id);
                            let param = vm.interns.get_str(param_id);
                            return Err(ExcType::type_error_duplicate_arg(func, param));
                        }
                        let (value, _) = value_guard.into_parts();
                        namespace[namespace_base + ns_idx] = value;
                        bound_params |= 1 << bit_idx;
                        continue 'kwargs;
                    }
                }
            }

            if let Some(excess_kwargs) = excess_kwargs {
                // Consume both value and key into **kwargs dict
                let (value, _) = value_guard.into_parts();
                let (key, vm) = key_guard.into_parts();
                excess_kwargs.set(key, value, vm)?;
                continue 'kwargs;
            }

            let func = vm.interns.get_str(func_name.name_id);
            let key_str = keyword_name.as_str(vm.interns);
            return Err(ExcType::type_error_unexpected_keyword(func, key_str));
        }

        // 3.4. Raise the deferred too-many-positional error now that every
        // kwarg has bound (or errored). CPython reports the range form when
        // some positional params have defaults, plus an `(and N keyword-only
        // argument(s))` suffix counting only the kw-only params *bound by the
        // call* — defaults have not been applied yet, exactly as in CPython's
        // `too_many_positional`.
        if positional_overflow {
            let kwonly_given = (0..self.kwarg_count())
                .filter(|i| (bound_params & (1 << (total_positional_params + i))) != 0)
                .count();
            let func = vm.interns.get_str(func_name.name_id);
            return Err(ExcType::type_error_too_many_positional_range(
                func,
                self.required_positional_count(),
                total_positional_params,
                positional_count,
                kwonly_given,
            ));
        }

        // 3.5. Apply default values to unbound optional parameters
        // Defaults layout: [pos_defaults...][arg_defaults...][kwarg_defaults...]
        // Each section only contains defaults for params that have them.
        let mut default_idx = 0;

        // Apply pos_args defaults (optional params at the end of pos_args)
        if self.pos_defaults_count > 0 {
            let first_optional = pos_param_count - self.pos_defaults_count;
            for i in first_optional..pos_param_count {
                if (bound_params & (1 << i)) == 0 {
                    namespace[namespace_base + i] = defaults[default_idx + (i - first_optional)].clone_with_heap(vm);
                    bound_params |= 1 << i;
                }
            }
        }
        default_idx += self.pos_defaults_count;

        // Apply args defaults (optional params at the end of args)
        if self.arg_defaults_count > 0 {
            let first_optional = arg_param_count - self.arg_defaults_count;
            for i in first_optional..arg_param_count {
                let ns_idx = pos_param_count + i;
                if (bound_params & (1 << ns_idx)) == 0 {
                    namespace[namespace_base + ns_idx] =
                        defaults[default_idx + (i - first_optional)].clone_with_heap(vm);
                    bound_params |= 1 << ns_idx;
                }
            }
        }
        default_idx += self.arg_defaults_count;

        // Apply kwargs defaults using the explicit default map
        if let Some(ref default_map) = self.kwarg_default_map {
            for (i, default_slot) in default_map.iter().enumerate() {
                if let Some(slot_idx) = default_slot {
                    let bound_idx = total_positional_params + i;
                    // Skip past *args slot if present
                    let ns_idx = total_positional_params + var_args_offset + i;
                    if (bound_params & (1 << bound_idx)) == 0 {
                        namespace[namespace_base + ns_idx] = defaults[default_idx + slot_idx].clone_with_heap(vm);
                        bound_params |= 1 << bound_idx;
                    }
                }
            }
        }

        // 4. Check that all required params are bound BEFORE building final namespace.
        // This ensures we can clean up properly on error without leaking heap values.

        // Check required positional params (pos_args + required args)
        let mut missing_positional: Vec<&str> = Vec::new();

        // Check pos_args
        if let Some(ref pos_args) = self.pos_args {
            let required_pos_only = pos_args.len().saturating_sub(self.pos_defaults_count);
            for (i, &param_id) in pos_args.iter().enumerate() {
                if i < required_pos_only && (bound_params & (1 << i)) == 0 {
                    missing_positional.push(vm.interns.get_str(param_id));
                }
            }
        }

        // Check args (positional-or-keyword)
        if let Some(ref args_params) = self.args {
            let required_args = args_params.len().saturating_sub(self.arg_defaults_count);
            for (i, &param_id) in args_params.iter().enumerate() {
                if i < required_args && (bound_params & (1 << (pos_param_count + i))) == 0 {
                    missing_positional.push(vm.interns.get_str(param_id));
                }
            }
        }

        if !missing_positional.is_empty() {
            // Clean up bound values before returning error
            let func = vm.interns.get_str(func_name.name_id);
            return Err(ExcType::type_error_missing_positional_with_names(
                func,
                &missing_positional,
            ));
        }

        // Check required keyword-only args
        let mut missing_kwonly: Vec<&str> = Vec::new();
        if let Some(ref kwargs_params) = self.kwargs {
            let default_map = self.kwarg_default_map.as_ref();
            for (i, &param_id) in kwargs_params.iter().enumerate() {
                let has_default = default_map.and_then(|map| map.get(i)).is_some_and(Option::is_some);
                if !has_default && (bound_params & (1 << (total_positional_params + i))) == 0 {
                    missing_kwonly.push(vm.interns.get_str(param_id));
                }
            }
        }

        if !missing_kwonly.is_empty() {
            let func = vm.interns.get_str(func_name.name_id);
            return Err(ExcType::type_error_missing_kwonly_with_names(func, &missing_kwonly));
        }

        // 5. Insert **kwargs dict if present (at the last slot)
        // Namespace layout: [pos_args][args][*args?][kwargs][**kwargs?]
        let (excess_kwargs, vm) = excess_kwargs_guard.into_parts();
        if let Some(excess_kwargs) = excess_kwargs {
            let dict_id = vm.heap.allocate(HeapData::Dict(excess_kwargs))?;
            let last_slot = namespace.len() - 1;
            namespace[last_slot] = Value::Ref(dict_id);
        }

        Ok(())
    }

    /// Returns the total number of named parameters (excluding *args/**kwargs slots).
    ///
    /// This is `pos_args.len() + args.len() + kwargs.len()`.
    pub fn param_count(&self) -> usize {
        self.pos_arg_count() + self.arg_count() + self.kwarg_count()
    }

    /// Returns the total number of namespace slots needed for parameters.
    ///
    /// This includes slots for:
    /// - All named parameters (pos_args + args + kwargs)
    /// - The *args tuple (if var_args is Some)
    /// - The **kwargs dict (if var_kwargs is Some)
    pub fn total_slots(&self) -> usize {
        let mut slots = self.param_count();
        if self.var_args.is_some() {
            slots += 1;
        }
        if self.var_kwargs.is_some() {
            slots += 1;
        }
        slots
    }

    /// Returns the total number of default values across all parameter groups.
    pub fn total_defaults_count(&self) -> usize {
        self.pos_defaults_count + self.arg_defaults_count + self.kwarg_defaults_count()
    }

    /// Returns the minimum number of positional arguments required.
    ///
    /// This is the total positional param count minus the number of defaults.
    /// For a signature like `def f(a, b, c=1)`, this returns 2 (a and b are required).
    #[inline]
    fn required_positional_count(&self) -> usize {
        self.pos_arg_count() + self.arg_count() - self.pos_defaults_count - self.arg_defaults_count
    }

    fn kwarg_defaults_count(&self) -> usize {
        self.kwarg_default_map
            .as_deref()
            .map(|v| v.iter().filter(|&x| x.is_some()).count())
            .unwrap_or_default()
    }

    /// Returns the number of positional-only parameters.
    fn pos_arg_count(&self) -> usize {
        self.pos_args.as_ref().map_or(0, Vec::len)
    }

    /// Returns the number of positional-or-keyword parameters.
    fn arg_count(&self) -> usize {
        self.args.as_ref().map_or(0, Vec::len)
    }

    /// Returns the number of keyword-only parameters.
    fn kwarg_count(&self) -> usize {
        self.kwargs.as_ref().map_or(0, Vec::len)
    }

    /// Returns an iterator over all parameter names in namespace slot order.
    ///
    /// Order: pos_args, args, var_args (if present), kwargs, var_kwargs (if present)
    fn param_names(&self) -> impl Iterator<Item = StringId> + '_ {
        let pos_args = self.pos_args.iter().flat_map(|v| v.iter().copied());
        let args = self.args.iter().flat_map(|v| v.iter().copied());
        let var_args = self.var_args.iter().copied();
        let kwargs = self.kwargs.iter().flat_map(|v| v.iter().copied());
        let var_kwargs = self.var_kwargs.iter().copied();

        pos_args.chain(args).chain(var_args).chain(kwargs).chain(var_kwargs)
    }

    /// Returns the maximum number of positional arguments accepted.
    ///
    /// Returns None if *args is present (unlimited positional args).
    fn max_positional_count(&self) -> Option<usize> {
        if self.var_args.is_some() {
            None
        } else {
            Some(self.pos_arg_count() + self.arg_count())
        }
    }

    /// Creates an error for wrong number of arguments.
    ///
    /// Handles both "missing required positional arguments" and "too many arguments" cases,
    /// formatting the error message to match CPython's style.
    ///
    /// # Arguments
    /// * `actual_count` - Number of arguments actually provided
    /// * `interns` - String storage for looking up interned names
    fn wrong_arg_count_error<T>(&self, actual_count: usize, interns: &Interns, func_name: Identifier) -> RunResult<T> {
        let name_str = interns.get_str(func_name.name_id);
        let param_count = self.param_count();
        // Only reached from the Simple/SimpleWithDefaults fast paths, so there
        // are no pos-only, kw-only, or `*args` params: every param is
        // positional-or-keyword and `param_count` is the positional maximum.
        let required = self.required_positional_count();
        let msg = if actual_count < required {
            // Missing arguments — CPython lists only the *required* params not
            // yet provided (params with defaults are never "missing").
            let missing_names: Vec<String> = self
                .param_names()
                .take(required)
                .skip(actual_count)
                .map(|string_id| interns.get_str(string_id).to_string())
                .collect();
            let missing_refs: Vec<&str> = missing_names.iter().map(String::as_str).collect();
            ExcType::missing_positional_msg(name_str, &missing_refs)
        } else {
            ExcType::too_many_positional_range_msg(name_str, required, param_count, actual_count, 0)
        };
        Err(SimpleException::new_msg(ExcType::TypeError, msg)
            .with_position(func_name.position)
            .into())
    }
}