Skip to main content

rustpython_vm/builtins/
tuple.rs

1use super::{
2    PositionIterInternal, PyGenericAlias, PyInt, PyStrRef, PyType, PyTypeRef, iter::builtins_iter,
3    locked_next,
4};
5use crate::common::lock::LazyLock;
6use crate::common::{hash, hash::PyHash, lock::PyMutex, wtf8::wtf8_concat};
7use crate::object::{Traverse, TraverseFn};
8use crate::{
9    AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject,
10    atomic_func,
11    class::{PyClassDef, PyClassImpl},
12    convert::{ToPyObject, TransmuteFromObject},
13    function::{FuncArgs, OptionalArg, PyArithmeticValue, PyComparisonValue, PySsize},
14    iter::PyExactSizeIterator,
15    protocol::{PyIterReturn, PyMappingMethods, PyNumberMethods, PySequenceMethods},
16    recursion::ReprGuard,
17    sequence::{OptionalRangeArgs, SequenceExt},
18    sliceable::{SequenceIndex, SliceableSequenceOp},
19    types::{
20        AsMapping, AsNumber, AsSequence, Comparable, Constructor, Hashable, IterNext, Iterable,
21        PyComparisonOp, Representable, SelfIter,
22    },
23    utils::collection_repr,
24    vm::VirtualMachine,
25};
26use alloc::fmt;
27use core::cell::{Cell, UnsafeCell};
28use core::ptr::NonNull;
29
30#[pyclass(module = false, name = "tuple", traverse = "manual")]
31pub struct PyTuple<R = PyObjectRef> {
32    elements: TupleElements<R>,
33}
34
35/// Tuple storage is immutable after publication, but marshal must publish a
36/// tuple in its reference table before recursively reading its children.
37/// This mirrors CPython's `PyTuple_New` followed by `PyTuple_SET_ITEM`.
38struct TupleElements<R>(UnsafeCell<Box<[R]>>);
39
40unsafe impl<R: Send> Send for TupleElements<R> {}
41unsafe impl<R: Sync> Sync for TupleElements<R> {}
42
43impl<R> TupleElements<R> {
44    const fn new(elements: Box<[R]>) -> Self {
45        Self(UnsafeCell::new(elements))
46    }
47
48    fn as_slice(&self) -> &[R] {
49        // SAFETY: initialization writes happen only while the tuple is owned by
50        // the synchronous marshal decoder; afterwards the storage is immutable.
51        unsafe { &*self.0.get() }
52    }
53
54    fn get_mut(&mut self) -> &mut Box<[R]> {
55        self.0.get_mut()
56    }
57
58    /// # Safety
59    /// The tuple must still be in its private initialization phase, and each
60    /// placeholder index must be replaced at most once before it is observable.
61    unsafe fn set_initializing(&self, index: usize, value: R) {
62        unsafe { (*self.0.get())[index] = value };
63    }
64}
65
66impl<R> core::ops::Deref for TupleElements<R> {
67    type Target = [R];
68
69    fn deref(&self) -> &Self::Target {
70        self.as_slice()
71    }
72}
73
74impl<'a, R> IntoIterator for &'a TupleElements<R> {
75    type Item = &'a R;
76    type IntoIter = core::slice::Iter<'a, R>;
77
78    fn into_iter(self) -> Self::IntoIter {
79        self.iter()
80    }
81}
82
83impl<R> fmt::Debug for PyTuple<R> {
84    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85        // TODO: implement more informational, non-recursive Debug formatter
86        f.write_str("tuple")
87    }
88}
89
90// SAFETY: Traverse properly visits all owned PyObjectRefs
91// Note: Only impl for PyTuple<PyObjectRef> (the default)
92unsafe impl Traverse for PyTuple {
93    fn traverse(&self, traverse_fn: &mut TraverseFn<'_>) {
94        self.elements.as_slice().traverse(traverse_fn);
95    }
96
97    fn clear(&mut self, out: &mut Vec<PyObjectRef>) {
98        let elements = core::mem::take(self.elements.get_mut());
99        if out.is_empty() {
100            // Reuse the allocation so deallocation does not need more memory.
101            *out = elements.into_vec();
102        } else {
103            out.extend(elements.into_vec());
104        }
105    }
106}
107
108thread_local! {
109    // A single freelist for all tuple sizes: `Py<PyTuple>` is a
110    // fixed-size allocation (elements are a separate boxed slice that is
111    // dropped and replaced on reuse), so husks are interchangeable.
112    // freelist_push must not read the payload — it runs after tp_clear,
113    // which has already emptied `elements`.
114    static TUPLE_FREELIST: Cell<crate::object::FreeList<PyTuple>> =
115        const { Cell::new(crate::object::FreeList::new()) };
116}
117
118impl PyPayload for PyTuple {
119    const MAX_FREELIST: usize = 2000;
120    const HAS_FREELIST: bool = true;
121
122    #[inline]
123    fn class(ctx: &Context) -> &'static Py<PyType> {
124        ctx.types.tuple_type
125    }
126
127    #[inline]
128    unsafe fn freelist_push(obj: *mut PyObject) -> bool {
129        TUPLE_FREELIST
130            .try_with(|fl| {
131                let mut list = fl.take();
132                let stored = if list.len() < Self::MAX_FREELIST {
133                    list.push(obj);
134                    true
135                } else {
136                    false
137                };
138                fl.set(list);
139                stored
140            })
141            .unwrap_or(false)
142    }
143
144    #[inline]
145    unsafe fn freelist_pop(_payload: &Self) -> Option<NonNull<PyObject>> {
146        TUPLE_FREELIST
147            .try_with(|fl| {
148                let mut list = fl.take();
149                let result = list.pop().map(|p| unsafe { NonNull::new_unchecked(p) });
150                fl.set(list);
151                result
152            })
153            .ok()
154            .flatten()
155    }
156}
157
158pub trait IntoPyTuple {
159    fn into_pytuple(self, vm: &VirtualMachine) -> PyTupleRef;
160}
161
162impl IntoPyTuple for () {
163    fn into_pytuple(self, vm: &VirtualMachine) -> PyTupleRef {
164        vm.ctx.empty_tuple.clone()
165    }
166}
167
168impl IntoPyTuple for Vec<PyObjectRef> {
169    fn into_pytuple(self, vm: &VirtualMachine) -> PyTupleRef {
170        PyTuple::new_ref(self, &vm.ctx)
171    }
172}
173
174pub trait FromPyTuple<'a>: Sized {
175    fn from_pytuple(tuple: &'a Py<PyTuple>, vm: &VirtualMachine) -> PyResult<Self>;
176}
177
178macro_rules! impl_from_into_pytuple {
179    ($($T:ident),+) => {
180        impl<$($T: ToPyObject),*> IntoPyTuple for ($($T,)*) {
181            fn into_pytuple(self, vm: &VirtualMachine) -> PyTupleRef {
182                #[allow(non_snake_case)]
183                let ($($T,)*) = self;
184                PyTuple::new_ref(vec![$($T.to_pyobject(vm)),*], &vm.ctx)
185            }
186        }
187
188        // TODO: figure out a way to let PyObjectRef implement TryFromBorrowedObject, and
189        //       have this be a TryFromBorrowedObject bound
190        impl<'a, $($T: TryFromObject),*> FromPyTuple<'a> for ($($T,)*) {
191            fn from_pytuple(tuple: &'a Py<PyTuple>, vm: &VirtualMachine) -> PyResult<Self> {
192                #[allow(non_snake_case)]
193                let &[$(ref $T),+] = tuple.as_slice().try_into().map_err(|_| {
194                    vm.new_type_error(format!("expected tuple with {} elements", impl_from_into_pytuple!(@count $($T)+)))
195                })?;
196                Ok(($($T::try_from_object(vm, $T.clone())?,)+))
197
198            }
199        }
200
201        impl<$($T: ToPyObject),*> ToPyObject for ($($T,)*) {
202            fn to_pyobject(self, vm: &VirtualMachine) -> PyObjectRef {
203                self.into_pytuple(vm).into()
204            }
205        }
206    };
207    (@count $($T:ident)+) => {
208        0 $(+ impl_from_into_pytuple!(@discard $T))+
209    };
210    (@discard $T:ident) => {
211        1
212    };
213}
214
215impl_from_into_pytuple!(A);
216impl_from_into_pytuple!(A, B);
217impl_from_into_pytuple!(A, B, C);
218impl_from_into_pytuple!(A, B, C, D);
219impl_from_into_pytuple!(A, B, C, D, E);
220impl_from_into_pytuple!(A, B, C, D, E, F);
221impl_from_into_pytuple!(A, B, C, D, E, F, G);
222
223pub type PyTupleRef = PyRef<PyTuple>;
224
225impl Constructor for PyTuple {
226    type Args = crate::function::PositionalIterable;
227
228    fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
229        let tuple_type = vm.ctx.types.tuple_type;
230        let uses_tuple_init = {
231            let cls_init = cls.slots.init.load().map(crate::types::fn_addr);
232            let tuple_init = tuple_type.slots.init.load().map(crate::types::fn_addr);
233            cls_init == tuple_init
234        };
235        let parsed: Self::Args = if cls.is(tuple_type) || uses_tuple_init {
236            args.bind_for(vm, Self::NAME)?
237        } else {
238            match args.args.as_slice() {
239                [] => Self::Args {
240                    iterable: OptionalArg::Missing,
241                },
242                [iterable] => Self::Args {
243                    iterable: OptionalArg::Present(iterable.clone()),
244                },
245                slice => {
246                    return Err(vm.new_arity_type_error(Self::NAME, 0..=1, slice.len()));
247                }
248            }
249        };
250        let iterable = parsed.iterable;
251
252        // Optimizations for exact tuple type
253        if cls.is(vm.ctx.types.tuple_type) {
254            // Return exact tuple as-is
255            if let OptionalArg::Present(ref input) = iterable
256                && let Ok(tuple) = input.clone().downcast_exact::<Self>(vm)
257            {
258                return Ok(tuple.into_pyref().into());
259            }
260
261            // Return empty tuple singleton
262            if iterable.is_missing() {
263                return Ok(vm.ctx.empty_tuple.clone().into());
264            }
265        }
266
267        let elements = if let OptionalArg::Present(iterable) = iterable {
268            iterable.try_to_value(vm)?
269        } else {
270            vec![]
271        };
272
273        // Return empty tuple singleton for exact tuple types (when iterable was empty)
274        if elements.is_empty() && cls.is(vm.ctx.types.tuple_type) {
275            return Ok(vm.ctx.empty_tuple.clone().into());
276        }
277
278        let payload = Self::from_elements(elements);
279        payload.into_ref_with_type(vm, cls).map(Into::into)
280    }
281
282    fn py_new(_cls: &Py<PyType>, args: Self::Args, vm: &VirtualMachine) -> PyResult<Self> {
283        let elements = if let OptionalArg::Present(iterable) = args.iterable {
284            iterable.try_to_value(vm)?
285        } else {
286            Vec::new()
287        };
288        Ok(Self::from_elements(elements))
289    }
290}
291
292impl PyTuple {
293    fn from_elements(elements: Vec<PyObjectRef>) -> Self {
294        Self {
295            elements: TupleElements::new(elements.into_boxed_slice()),
296        }
297    }
298}
299
300impl<R> AsRef<[R]> for PyTuple<R> {
301    fn as_ref(&self) -> &[R] {
302        &self.elements
303    }
304}
305
306impl<R> core::ops::Deref for PyTuple<R> {
307    type Target = [R];
308
309    fn deref(&self) -> &[R] {
310        &self.elements
311    }
312}
313
314impl<'a, R> core::iter::IntoIterator for &'a PyTuple<R> {
315    type Item = &'a R;
316    type IntoIter = core::slice::Iter<'a, R>;
317
318    fn into_iter(self) -> Self::IntoIter {
319        self.as_slice().iter()
320    }
321}
322
323impl<'a, R> core::iter::IntoIterator for &'a Py<PyTuple<R>> {
324    type Item = &'a R;
325    type IntoIter = core::slice::Iter<'a, R>;
326
327    fn into_iter(self) -> Self::IntoIter {
328        self.as_slice().iter()
329    }
330}
331
332impl<R> PyTuple<R> {
333    #[must_use]
334    pub fn as_slice(&self) -> &[R] {
335        &self.elements
336    }
337}
338
339impl PyTuple<PyObjectRef> {
340    // Do not deprecate this. empty_tuple must be checked.
341    pub fn new_ref(elements: Vec<PyObjectRef>, ctx: &Context) -> PyRef<Self> {
342        if elements.is_empty() {
343            ctx.empty_tuple.clone()
344        } else {
345            let elements = elements.into_boxed_slice();
346            PyRef::new_ref(
347                Self {
348                    elements: TupleElements::new(elements),
349                },
350                ctx.types.tuple_type.to_owned(),
351                None,
352            )
353        }
354    }
355
356    /// Creating a new tuple with given boxed slice.
357    /// NOTE: for usual case, you probably want to use PyTuple::new_ref.
358    /// Calling this function implies trying micro optimization for non-zero-sized tuple.
359    #[must_use]
360    pub const fn new_unchecked(elements: Box<[PyObjectRef]>) -> Self {
361        Self {
362            elements: TupleElements::new(elements),
363        }
364    }
365
366    /// # Safety
367    /// This tuple must be a marshal placeholder which has not escaped the
368    /// decoder, and `index` must not have been replaced previously.
369    pub(crate) unsafe fn set_marshal_item(&self, index: usize, value: PyObjectRef) {
370        unsafe { self.elements.set_initializing(index, value) };
371    }
372
373    fn repeat(zelf: PyRef<Self>, value: isize, vm: &VirtualMachine) -> PyResult<PyRef<Self>> {
374        Ok(if zelf.as_slice().is_empty() || value == 0 {
375            vm.ctx.empty_tuple.clone()
376        } else if value == 1 && zelf.class().is(vm.ctx.types.tuple_type) {
377            // Special case: when some `tuple` is multiplied by `1`,
378            // nothing really happens, we need to return an object itself
379            // with the same `id()` to be compatible with CPython.
380            // This only works for `tuple` itself, not its subclasses.
381            zelf
382        } else {
383            let v = zelf.as_slice().mul(vm, value)?;
384            let elements = v.into_boxed_slice();
385            Self {
386                elements: TupleElements::new(elements),
387            }
388            .into_ref(&vm.ctx)
389        })
390    }
391}
392
393impl<R> Py<PyTuple<R>> {
394    #[inline]
395    pub fn as_slice(&self) -> &[R] {
396        self.payload.as_slice()
397    }
398}
399
400impl Py<PyTuple> {
401    pub fn extract_tuple<'a, T: FromPyTuple<'a>>(&'a self, vm: &VirtualMachine) -> PyResult<T> {
402        T::from_pytuple(self, vm)
403    }
404}
405
406impl<T> PyTuple<PyRef<T>> {
407    pub(crate) fn new_ref_typed_with_type(
408        elements: Vec<PyRef<T>>,
409        tuple_type: PyTypeRef,
410    ) -> PyRef<Self> {
411        // SAFETY: PyRef<T> has the same layout as PyObjectRef.
412        unsafe {
413            let elements: Vec<PyObjectRef> =
414                core::mem::transmute::<Vec<PyRef<T>>, Vec<PyObjectRef>>(elements);
415            let tuple = PyRef::new_ref(
416                PyTuple::new_unchecked(elements.into_boxed_slice()),
417                tuple_type,
418                None,
419            );
420            core::mem::transmute::<PyRef<PyTuple>, PyRef<Self>>(tuple)
421        }
422    }
423
424    pub fn new_ref_typed(elements: Vec<PyRef<T>>, ctx: &Context) -> PyRef<Self> {
425        // SAFETY: PyRef<T> has the same layout as PyObjectRef
426        unsafe {
427            let elements: Vec<PyObjectRef> =
428                core::mem::transmute::<Vec<PyRef<T>>, Vec<PyObjectRef>>(elements);
429            let tuple = PyTuple::<PyObjectRef>::new_ref(elements, ctx);
430            core::mem::transmute::<PyRef<PyTuple>, PyRef<Self>>(tuple)
431        }
432    }
433}
434
435impl PyTuple {
436    fn __add__(
437        zelf: PyRef<Self>,
438        other: PyObjectRef,
439        vm: &VirtualMachine,
440    ) -> PyArithmeticValue<PyRef<Self>> {
441        let added = other.downcast::<Self>().map(|other| {
442            if other.as_slice().is_empty() && zelf.class().is(vm.ctx.types.tuple_type) {
443                zelf
444            } else if zelf.as_slice().is_empty() && other.class().is(vm.ctx.types.tuple_type) {
445                other
446            } else {
447                let elements = zelf
448                    .as_slice()
449                    .iter()
450                    .chain(other.as_slice())
451                    .cloned()
452                    .collect::<Box<[_]>>();
453                Self {
454                    elements: TupleElements::new(elements),
455                }
456                .into_ref(&vm.ctx)
457            }
458        });
459        PyArithmeticValue::from_option(added.ok())
460    }
461
462    #[inline]
463    #[must_use]
464    pub fn __len__(&self) -> usize {
465        self.as_slice().len()
466    }
467
468    fn __mul__(zelf: PyRef<Self>, value: PySsize, vm: &VirtualMachine) -> PyResult<PyRef<Self>> {
469        Self::repeat(zelf, value, vm)
470    }
471
472    fn _getitem(&self, needle: &PyObject, vm: &VirtualMachine) -> PyResult {
473        match SequenceIndex::try_from_borrowed_object(vm, needle, "tuple")? {
474            SequenceIndex::Int(i) => {
475                let index = self
476                    .elements
477                    .wrap_index(i)
478                    .ok_or_else(|| vm.new_index_error("tuple index out of range"))?;
479                Ok(self.elements[index].clone())
480            }
481            SequenceIndex::Slice(slice) => self
482                .elements
483                .getitem_by_slice(vm, slice)
484                .map(|x| vm.ctx.new_tuple(x).into()),
485        }
486    }
487
488    fn __getitem__(&self, needle: &PyObject, vm: &VirtualMachine) -> PyResult {
489        self._getitem(needle, vm)
490    }
491
492    fn _contains(&self, needle: &PyObject, vm: &VirtualMachine) -> PyResult<bool> {
493        for element in &self.elements {
494            if vm.identical_or_equal(element, needle)? {
495                return Ok(true);
496            }
497        }
498        Ok(false)
499    }
500
501    fn __contains__(&self, needle: &PyObject, vm: &VirtualMachine) -> PyResult<bool> {
502        self._contains(needle, vm)
503    }
504}
505
506#[pyclass(
507    itemsize = core::mem::size_of::<crate::PyObjectRef>(),
508    flags(BASETYPE, SEQUENCE, _MATCH_SELF),
509    with(AsMapping, AsNumber, AsSequence, Hashable, Comparable, Iterable, Constructor, Representable)
510)]
511impl Py<PyTuple> {
512    #[pymethod]
513    fn count(&self, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<usize> {
514        let mut count: usize = 0;
515        for element in self {
516            if vm.identical_or_equal(element, &value)? {
517                count += 1;
518            }
519        }
520        Ok(count)
521    }
522
523    #[pymethod]
524    fn index(
525        &self,
526        value: PyObjectRef,
527        range: OptionalRangeArgs,
528        vm: &VirtualMachine,
529    ) -> PyResult<usize> {
530        let (start, stop) = range.saturate(self.as_slice().len(), vm)?;
531        for (index, element) in self.as_slice().iter().enumerate().take(stop).skip(start) {
532            if vm.identical_or_equal(element, &value)? {
533                return Ok(index);
534            }
535        }
536        Err(vm.new_value_error("tuple.index(x): x not in tuple"))
537    }
538
539    #[pymethod]
540    fn __getnewargs__(zelf: PyRef<PyTuple>, vm: &VirtualMachine) -> (PyTupleRef,) {
541        // the arguments to pass to tuple() is just one tuple - so we'll be doing tuple(tup), which
542        // should just return tup, or tuple_subclass(tup), which'll copy/validate (e.g. for a
543        // structseq)
544        let tup_arg = if zelf.class().is(vm.ctx.types.tuple_type) {
545            zelf
546        } else {
547            PyTuple::new_ref(zelf.as_slice().to_vec(), &vm.ctx)
548        };
549        (tup_arg,)
550    }
551
552    #[pyclassmethod]
553    fn __class_getitem__(
554        cls: PyTypeRef,
555        object: PyObjectRef,
556        vm: &VirtualMachine,
557    ) -> PyResult<PyGenericAlias> {
558        PyGenericAlias::from_args(cls, object, vm)
559    }
560}
561
562impl AsMapping for PyTuple {
563    fn as_mapping() -> &'static PyMappingMethods {
564        static AS_MAPPING: LazyLock<PyMappingMethods> = LazyLock::new(|| PyMappingMethods {
565            length: atomic_func!(|mapping, _vm| {
566                Ok(PyTuple::mapping_downcast(mapping).as_slice().len())
567            }),
568            subscript: atomic_func!(|mapping, needle, vm| PyTuple::mapping_downcast(mapping)
569                .payload
570                ._getitem(needle, vm)),
571            ..PyMappingMethods::NOT_IMPLEMENTED
572        });
573        &AS_MAPPING
574    }
575}
576
577impl AsSequence for PyTuple {
578    fn as_sequence() -> &'static PySequenceMethods {
579        static AS_SEQUENCE: LazyLock<PySequenceMethods> = LazyLock::new(|| PySequenceMethods {
580            length: atomic_func!(|seq, _vm| Ok(PyTuple::sequence_downcast(seq).as_slice().len())),
581            concat: atomic_func!(|seq, other, vm| {
582                let zelf = PyTuple::sequence_downcast(seq);
583                match PyTuple::__add__(zelf.to_owned(), other.to_owned(), vm) {
584                    PyArithmeticValue::Implemented(tuple) => Ok(tuple.into()),
585                    PyArithmeticValue::NotImplemented => Err(vm.new_type_error(format!(
586                        "can only concatenate tuple (not '{}') to tuple",
587                        other.class().name()
588                    ))),
589                }
590            }),
591            repeat: atomic_func!(|seq, n, vm| {
592                let zelf = PyTuple::sequence_downcast(seq);
593                PyTuple::repeat(zelf.to_owned(), n, vm).map(|x| x.into())
594            }),
595            item: atomic_func!(|seq, i, vm| {
596                let zelf = PyTuple::sequence_downcast(seq);
597                zelf.as_slice().getitem_by_index(vm, i)
598            }),
599            contains: atomic_func!(|seq, needle, vm| {
600                let zelf = PyTuple::sequence_downcast(seq);
601                zelf.payload._contains(needle, vm)
602            }),
603            ..PySequenceMethods::NOT_IMPLEMENTED
604        });
605        &AS_SEQUENCE
606    }
607}
608
609impl AsNumber for PyTuple {
610    fn as_number() -> &'static PyNumberMethods {
611        static AS_NUMBER: PyNumberMethods = PyNumberMethods {
612            boolean: Some(|number, _vm| {
613                let zelf = number.obj.downcast_ref::<PyTuple>().unwrap();
614                Ok(!zelf.as_slice().is_empty())
615            }),
616            ..PyNumberMethods::NOT_IMPLEMENTED
617        };
618        &AS_NUMBER
619    }
620}
621
622impl Hashable for PyTuple {
623    #[inline]
624    fn hash(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyHash> {
625        tuple_hash(zelf.as_slice(), vm)
626    }
627}
628
629impl Comparable for PyTuple {
630    fn cmp(
631        zelf: &Py<Self>,
632        other: &PyObject,
633        op: PyComparisonOp,
634        vm: &VirtualMachine,
635    ) -> PyResult<PyComparisonValue> {
636        if let Some(res) = op.identical_optimization(zelf, other) {
637            return Ok(res.into());
638        }
639        let other = class_or_notimplemented!(Self, other);
640        zelf.as_slice()
641            .iter()
642            .map(|o| &**o)
643            .richcompare(other.as_slice().iter().map(|o| &**o), op, vm)
644            .map(PyComparisonValue::Implemented)
645    }
646}
647
648impl Iterable for PyTuple {
649    fn iter(zelf: PyRef<Self>, vm: &VirtualMachine) -> PyResult {
650        Ok(PyTupleIterator {
651            internal: PyMutex::new(PositionIterInternal::new(zelf, 0)),
652        }
653        .into_pyobject(vm))
654    }
655}
656
657impl Representable for PyTuple {
658    #[inline]
659    fn repr(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyStrRef> {
660        let s = if zelf.as_slice().is_empty() {
661            vm.ctx.intern_str("()").to_owned()
662        } else if let Some(_guard) = ReprGuard::enter(vm, zelf.as_object()) {
663            let s = if zelf.as_slice().len() == 1 {
664                wtf8_concat!("(", zelf.as_slice()[0].repr(vm)?.as_wtf8(), ",)")
665            } else {
666                collection_repr(
667                    None,
668                    "(",
669                    ")",
670                    "()",
671                    zelf.as_slice().iter().map(|o| &**o),
672                    vm,
673                )?
674            };
675            vm.ctx.new_str(s)
676        } else {
677            vm.ctx.intern_str("(...)").to_owned()
678        };
679        Ok(s)
680    }
681
682    #[cold]
683    fn repr_str(_zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<String> {
684        unreachable!("use repr instead")
685    }
686}
687
688impl PyRef<PyTuple<PyObjectRef>> {
689    pub fn try_into_typed<T: PyPayload>(
690        self,
691        vm: &VirtualMachine,
692    ) -> PyResult<PyRef<PyTuple<PyRef<T>>>> {
693        // Check that all elements are of the correct type
694        for elem in self.as_slice() {
695            <PyRef<T> as TransmuteFromObject>::check(vm, elem)?;
696        }
697        // SAFETY: We just verified all elements are of type T
698        Ok(unsafe { core::mem::transmute::<Self, PyRef<PyTuple<PyRef<T>>>>(self) })
699    }
700}
701
702impl<T: PyPayload> PyRef<PyTuple<PyRef<T>>> {
703    #[must_use]
704    pub fn into_untyped(self) -> PyRef<PyTuple> {
705        // SAFETY: PyTuple<PyRef<T>> has the same layout as PyTuple
706        unsafe { core::mem::transmute::<Self, PyRef<PyTuple>>(self) }
707    }
708}
709
710impl<T: PyPayload> Py<PyTuple<PyRef<T>>> {
711    pub fn as_untyped(&self) -> &Py<PyTuple> {
712        // SAFETY: PyTuple<PyRef<T>> has the same layout as PyTuple
713        unsafe { core::mem::transmute::<&Self, &Py<PyTuple>>(self) }
714    }
715}
716
717impl<T: PyPayload> From<PyRef<PyTuple<PyRef<T>>>> for PyTupleRef {
718    #[inline]
719    fn from(tup: PyRef<PyTuple<PyRef<T>>>) -> Self {
720        tup.into_untyped()
721    }
722}
723
724#[pyclass(module = false, name = "tuple_iterator", traverse)]
725#[derive(Debug)]
726pub(crate) struct PyTupleIterator {
727    internal: PyMutex<PositionIterInternal<PyTupleRef>>,
728}
729
730impl PyPayload for PyTupleIterator {
731    fn class(ctx: &Context) -> &'static Py<PyType> {
732        ctx.types.tuple_iterator_type
733    }
734}
735
736#[pyclass(flags(DISALLOW_INSTANTIATION), with(IterNext, Iterable))]
737impl Py<PyTupleIterator> {
738    #[pymethod]
739    fn __length_hint__(&self) -> usize {
740        self.internal.lock().length_hint(|obj| obj.as_slice().len())
741    }
742
743    #[pymethod]
744    fn __setstate__(&self, object: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
745        self.internal
746            .lock()
747            .set_state(&object, |obj, pos| pos.min(obj.as_slice().len()), vm)
748    }
749
750    #[pymethod]
751    fn __reduce__(&self, vm: &VirtualMachine) -> PyResult<PyTupleRef> {
752        let func = builtins_iter(vm)?;
753        Ok(self.internal.lock().reduce(
754            func,
755            |x| x.clone().into(),
756            |vm| vm.ctx.empty_tuple.clone().into(),
757            vm,
758        ))
759    }
760}
761
762impl PyTupleIterator {
763    /// Fast path for FOR_ITER specialization.
764    pub(crate) fn fast_next(&self) -> Option<PyObjectRef> {
765        locked_next(&self.internal, |tuple, pos| {
766            Ok(PyIterReturn::from_result(
767                tuple.as_slice().get(pos).cloned().ok_or(None),
768            ))
769        })
770        .ok()
771        .and_then(|r| match r {
772            PyIterReturn::Return(v) => Some(v),
773            PyIterReturn::StopIteration(_) => None,
774        })
775    }
776}
777
778impl SelfIter for PyTupleIterator {}
779impl IterNext for PyTupleIterator {
780    fn next(zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<PyIterReturn> {
781        locked_next(&zelf.internal, |tuple, pos| {
782            Ok(PyIterReturn::from_result(
783                tuple.as_slice().get(pos).cloned().ok_or(None),
784            ))
785        })
786    }
787}
788
789fn vectorcall_tuple(
790    zelf_obj: &PyObject,
791    args: Vec<PyObjectRef>,
792    nargs: usize,
793    kwnames: Option<&[PyObjectRef]>,
794    vm: &VirtualMachine,
795) -> PyResult {
796    let zelf: &Py<PyType> = zelf_obj.downcast_ref().unwrap();
797    let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames);
798    // Use the type's own slot_new rather than calling PyTuple::slot_new directly,
799    // so Rust-level subclasses (e.g. struct sequences) get their custom slot_new called.
800    (zelf.slots.new.load().unwrap())(zelf.to_owned(), func_args, vm)
801}
802
803pub(crate) fn init(context: &'static Context) {
804    PyTuple::extend_class(context, context.types.tuple_type);
805    PyTupleIterator::extend_class(context, context.types.tuple_iterator_type);
806    context
807        .types
808        .tuple_type
809        .slots
810        .vectorcall
811        .store(Some(vectorcall_tuple));
812}
813
814pub(super) fn tuple_hash(elements: &[PyObjectRef], vm: &VirtualMachine) -> PyResult<PyHash> {
815    hash::hash_tuple(elements.iter().map(|val| element_hash(val, vm)))
816}
817
818/// Hash a single tuple element, skipping the generic recursion guard for
819/// exact `int`s. `int.__hash__` is a leaf computation - it can't recurse or
820/// call back into arbitrary Python code - so `PyObject::hash`'s native-stack
821/// depth check is pure overhead here. This matters because tuples used as
822/// dict keys (e.g. state tuples in `pyperformance`'s `mdp` benchmark) are
823/// often deeply nested namedtuples whose leaves are plain ints.
824#[inline]
825fn element_hash(val: &PyObject, vm: &VirtualMachine) -> PyResult<PyHash> {
826    if val.class().is(vm.ctx.types.int_type)
827        && let Some(i) = val.downcast_ref::<PyInt>()
828    {
829        return Ok(hash::hash_bigint(i.as_bigint()));
830    }
831    val.hash(vm)
832}