use std::{
cell::Cell,
cmp::Ordering,
collections::hash_map::DefaultHasher,
fmt::Write,
hash::{Hash, Hasher},
mem,
};
use smallvec::SmallVec;
use super::{CmpOrder, MontyIter, PyTrait};
use crate::{
args::ArgValues,
bytecode::{CallResult, ContainsVM, DropWithVM, RecursionToken, VM},
defer_drop, defer_drop_vm_mut,
exception_private::{ExcType, RunResult},
hash::HashValue,
heap::{DropWithHeap, Heap, HeapData, HeapId, HeapItem, HeapRead, HeapReadOutput},
intern::StaticStrings,
resource::{ResourceError, ResourceTracker},
types::{
LazyHeapSet, Type,
list::repr_sequence_fmt,
slice::{normalize_sequence_index, slice_collect_iterator},
},
value::{EitherStr, Value},
};
const TUPLE_INLINE_CAPACITY: usize = 3;
pub(crate) type TupleVec = SmallVec<[Value; TUPLE_INLINE_CAPACITY]>;
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
pub(crate) struct Tuple {
items: TupleVec,
contains_refs: bool,
#[serde(skip)]
cached_hash: Cell<Option<HashValue>>,
}
impl Tuple {
#[must_use]
fn new(items: TupleVec) -> Self {
let contains_refs = items.iter().any(|v| matches!(v, Value::Ref(_)));
Self {
items,
contains_refs,
cached_hash: Cell::new(None),
}
}
#[must_use]
pub fn as_slice(&self) -> &[Value] {
&self.items
}
#[inline]
#[must_use]
pub fn contains_refs(&self) -> bool {
self.contains_refs
}
pub fn init(vm: &mut VM<'_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {
let value = args.get_zero_one_arg("tuple", vm.heap)?;
match value {
None => {
Ok(vm.heap.get_empty_tuple())
}
Some(v) => {
let items = MontyIter::new(v, vm)?.collect(vm)?;
Ok(allocate_tuple(items, vm.heap)?)
}
}
}
}
impl From<Tuple> for Vec<Value> {
fn from(tuple: Tuple) -> Self {
tuple.items.into_vec()
}
}
impl From<Tuple> for TupleVec {
fn from(tuple: Tuple) -> Self {
tuple.items
}
}
pub fn allocate_tuple(
items: SmallVec<[Value; TUPLE_INLINE_CAPACITY]>,
heap: &Heap<impl ResourceTracker>,
) -> Result<Value, ResourceError> {
if items.is_empty() {
Ok(heap.get_empty_tuple())
} else {
let heap_id = heap.allocate(HeapData::Tuple(Tuple::new(items)))?;
Ok(Value::Ref(heap_id))
}
}
impl<'h> HeapRead<'h, Tuple> {
pub(crate) fn clone_item(&self, index: usize, vm: &mut VM<'h, impl ResourceTracker>) -> Value {
self.get(vm.heap).items[index].clone_with_heap(vm)
}
fn clone_all_items(&self, vm: &mut VM<'h, impl ResourceTracker>) -> TupleVec {
let len = self.get(vm.heap).items.len();
let mut result = TupleVec::with_capacity(len);
for i in 0..len {
result.push(self.clone_item(i, vm));
}
result
}
#[expect(clippy::iter_not_returning_iterator)]
pub(crate) fn iter<R: ResourceTracker>(&self, vm: &mut VM<'h, R>) -> RunResult<TupleIter<'_, 'h>> {
TupleIter::new(self, vm)
}
}
pub(crate) struct TupleIter<'a, 'h> {
tuple: &'a HeapRead<'h, Tuple>,
index: usize,
token: RecursionToken,
current: Value,
}
impl<'a, 'h> TupleIter<'a, 'h> {
fn new<R: ResourceTracker>(tuple: &'a HeapRead<'h, Tuple>, vm: &mut VM<'h, R>) -> RunResult<Self> {
let token = vm.recursion_token()?;
Ok(Self {
tuple,
index: 0,
token,
current: Value::Undefined,
})
}
pub(crate) fn next<'i, R: ResourceTracker>(&'i mut self, vm: &mut VM<'h, R>) -> RunResult<Option<&'i Value>> {
mem::replace(&mut self.current, Value::Undefined).drop_with_heap(vm.heap);
vm.heap.check_time()?;
let items = &self.tuple.get(vm.heap).items;
if self.index >= items.len() {
return Ok(None);
}
self.current = items[self.index].clone_with_heap(vm.heap);
self.index += 1;
Ok(Some(&self.current))
}
pub(crate) fn next_with_index<'i, R: ResourceTracker>(
&'i mut self,
vm: &mut VM<'h, R>,
) -> RunResult<Option<(usize, &'i Value)>> {
let position = self.index;
Ok(self.next(vm)?.map(|item| (position, item)))
}
}
impl<'h> DropWithVM<'h> for TupleIter<'_, 'h> {
fn drop_with_vm(self, container: &mut impl ContainsVM<'h>) {
self.current.drop_with_heap(container);
self.token.drop_with_vm(container);
}
}
impl<'h> PyTrait<'h> for HeapRead<'h, Tuple> {
fn py_type(&self, _vm: &VM<'h, impl ResourceTracker>) -> Type {
Type::Tuple
}
fn py_len(&self, vm: &VM<'h, impl ResourceTracker>) -> Option<usize> {
Some(self.get(vm.heap).items.len())
}
fn py_getitem(&self, key: &Value, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<Value> {
if let Value::Ref(key_id) = key
&& let HeapData::Slice(slice_obj) = vm.heap.get(*key_id)
{
let items =
slice_collect_iterator(vm, slice_obj, self.get(vm.heap).items.iter(), |v| v.clone_with_heap(vm))?;
return Ok(allocate_tuple(items, vm.heap)?);
}
let index = key.as_index(vm, Type::Tuple)?;
let len = self.get(vm.heap).as_slice().len();
let len_i64 = i64::try_from(len).expect("tuple length exceeds i64::MAX");
let normalized = if index < 0 { index + len_i64 } else { index };
if normalized < 0 || normalized >= len_i64 {
return Err(ExcType::tuple_index_error());
}
let idx = usize::try_from(normalized).expect("tuple index validated non-negative");
Ok(self.clone_item(idx, vm))
}
fn py_eq_impl(&self, other: &Value, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<Option<bool>> {
let Some(HeapReadOutput::Tuple(other)) = other.read_heap(vm) else {
return Ok(None);
};
if self.get(vm.heap).items.len() != other.get(vm.heap).items.len() {
return Ok(Some(false));
}
let iter = self.iter(vm)?;
defer_drop_vm_mut!(iter, vm);
while let Some((i, a)) = iter.next_with_index(vm)? {
let b = other.clone_item(i, vm);
defer_drop!(b, vm);
if !a.py_eq(b, vm)? {
return Ok(Some(false));
}
}
Ok(Some(true))
}
fn py_hash(&self, _self_id: HeapId, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<Option<HashValue>> {
if let Some(cached) = self.get(vm.heap).cached_hash.get() {
return Ok(Some(cached));
}
let mut hasher = DefaultHasher::new();
let iter = self.iter(vm)?;
defer_drop_vm_mut!(iter, vm);
while let Some(item) = iter.next(vm)? {
match item.py_hash(vm)? {
Some(h) => h.hash(&mut hasher),
None => return Ok(None),
}
}
let hash = HashValue::new(hasher.finish());
self.get(vm.heap).cached_hash.set(Some(hash));
Ok(Some(hash))
}
fn py_cmp(&self, other: &Self, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<CmpOrder> {
let a_len = self.get(vm.heap).items.len();
let b_len = other.get(vm.heap).items.len();
let min_len = a_len.min(b_len);
let iter = self.iter(vm)?;
defer_drop_vm_mut!(iter, vm);
while let Some((i, av)) = iter.next_with_index(vm)? {
if i >= min_len {
break;
}
let bv = other.clone_item(i, vm);
defer_drop!(bv, vm);
match av.py_cmp(bv, vm)? {
CmpOrder::Ordered(Ordering::Equal) => {}
CmpOrder::Ordered(ord) => return Ok(CmpOrder::Ordered(ord)),
CmpOrder::Unordered => return Ok(CmpOrder::Unordered),
CmpOrder::Incomparable => {
if !av.py_eq(bv, vm)? {
return Ok(CmpOrder::Incomparable);
}
}
}
}
Ok(CmpOrder::Ordered(a_len.cmp(&b_len)))
}
fn py_add(&self, other: &Self, vm: &mut VM<'h, impl ResourceTracker>) -> Result<Option<Value>, ResourceError> {
let mut items = self.clone_all_items(vm);
items.extend(other.clone_all_items(vm));
Ok(Some(allocate_tuple(items, vm.heap)?))
}
fn py_call_attr(
&mut self,
_self_id: HeapId,
vm: &mut VM<'h, impl ResourceTracker>,
attr: &EitherStr,
args: ArgValues,
) -> RunResult<CallResult> {
match attr.static_string() {
Some(StaticStrings::Index) => tuple_index(self, args, vm).map(CallResult::Value),
Some(StaticStrings::Count) => tuple_count(self, args, vm).map(CallResult::Value),
_ => {
args.drop_with_heap(vm);
Err(ExcType::attribute_error(Type::Tuple, attr.as_str(vm.interns)))
}
}
}
fn py_bool(&self, vm: &mut VM<'h, impl ResourceTracker>) -> bool {
!self.get(vm.heap).items.is_empty()
}
fn py_repr_fmt(
&self,
f: &mut impl Write,
vm: &mut VM<'h, impl ResourceTracker>,
heap_ids: &mut LazyHeapSet,
) -> RunResult<()> {
let len = self.get(vm.heap).as_slice().len();
if len == 1 {
let Ok(mut guard) = vm.recursion_guard() else {
return Ok(f.write_str("...")?);
};
let vm = &mut *guard;
write!(f, "(")?;
let item = self.clone_item(0, vm);
defer_drop!(item, vm);
item.py_repr_fmt(f, vm, heap_ids)?;
write!(f, ",)")?;
return Ok(());
}
repr_sequence_fmt('(', ')', len, |heap, i| &self.get(heap).as_slice()[i], f, vm, heap_ids)
}
}
impl HeapItem for Tuple {
fn py_estimate_size(&self) -> usize {
mem::size_of::<Self>() + self.items.len() * mem::size_of::<Value>()
}
fn py_dec_ref_ids(&mut self, stack: &mut Vec<HeapId>) {
if !self.contains_refs {
return;
}
for obj in &mut self.items {
if let Value::Ref(id) = obj {
stack.push(*id);
#[cfg(feature = "memory-model-checks")]
obj.dec_ref_forget();
}
}
}
}
fn tuple_index<'h>(
tuple: &HeapRead<'h, Tuple>,
args: ArgValues,
vm: &mut VM<'h, impl ResourceTracker>,
) -> RunResult<Value> {
let pos_args = args.into_pos_only("tuple.index", vm.heap)?;
defer_drop!(pos_args, vm);
let len = tuple.get(vm.heap).as_slice().len();
let (value, start, end) = match pos_args.as_slice() {
[] => return Err(ExcType::type_error_at_least("tuple.index", 1, 0)),
[value] => (value, 0, len),
[value, start_arg] => {
let start = normalize_sequence_index(start_arg.as_int(vm)?, len);
(value, start, len)
}
[value, start_arg, end_arg] => {
let start = normalize_sequence_index(start_arg.as_int(vm)?, len);
let end = normalize_sequence_index(end_arg.as_int(vm)?, len).max(start);
(value, start, end)
}
other => return Err(ExcType::type_error_at_most("tuple.index", 3, other.len())),
};
let iter = tuple.iter(vm)?;
defer_drop_vm_mut!(iter, vm);
while let Some((idx, item)) = iter.next_with_index(vm)? {
if idx >= end {
break;
}
if idx >= start && value.py_eq(item, vm)? {
let idx_i64 = i64::try_from(idx).expect("index exceeds i64::MAX");
return Ok(Value::Int(idx_i64));
}
}
Err(ExcType::value_error_not_in_tuple())
}
fn tuple_count<'h>(
tuple: &HeapRead<'h, Tuple>,
args: ArgValues,
vm: &mut VM<'h, impl ResourceTracker>,
) -> RunResult<Value> {
let value = args.get_one_arg("tuple.count", vm.heap)?;
defer_drop!(value, vm);
let mut count = 0usize;
let iter = tuple.iter(vm)?;
defer_drop_vm_mut!(iter, vm);
while let Some(item) = iter.next(vm)? {
if value.py_eq(item, vm)? {
count += 1;
}
}
let count_i64 = i64::try_from(count).expect("count exceeds i64::MAX");
Ok(Value::Int(count_i64))
}