use std::{
cell::Cell,
collections::hash_map::DefaultHasher,
fmt::Write,
hash::{Hash, Hasher},
mem,
};
use super::PyTrait;
use crate::{
bytecode::{CallResult, ContainsVM, DropWithVM, RecursionToken, VM},
defer_drop, defer_drop_vm_mut,
exception_private::{ExcType, RunResult},
hash::HashValue,
heap::{HeapId, HeapItem, HeapRead, HeapReadOutput},
intern::{Interns, StringId},
resource::ResourceTracker,
types::{Type, py_trait::LazyHeapSet},
value::{EitherStr, Value},
};
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub(crate) struct NamedTuple {
name: EitherStr,
field_names: Vec<EitherStr>,
items: Vec<Value>,
contains_refs: bool,
#[serde(skip)]
cached_hash: Cell<Option<HashValue>>,
}
impl NamedTuple {
#[must_use]
pub fn new(name: impl Into<EitherStr>, field_names: Vec<EitherStr>, items: Vec<Value>) -> Self {
assert_eq!(
field_names.len(),
items.len(),
"NamedTuple field_names and items must have same length"
);
let contains_refs = items.iter().any(|v| matches!(v, Value::Ref(_)));
Self {
name: name.into(),
field_names,
items,
contains_refs,
cached_hash: Cell::new(None),
}
}
#[must_use]
pub fn name<'a>(&'a self, interns: &'a Interns) -> &'a str {
self.name.as_str(interns)
}
#[must_use]
pub fn field_names(&self) -> &[EitherStr] {
&self.field_names
}
#[must_use]
pub fn as_vec(&self) -> &Vec<Value> {
&self.items
}
#[must_use]
pub fn len(&self) -> usize {
self.items.len()
}
#[inline]
#[must_use]
pub fn contains_refs(&self) -> bool {
self.contains_refs
}
#[must_use]
pub fn get_by_name(&self, name_str: &str, interns: &Interns) -> Option<&Value> {
self.field_names
.iter()
.position(|field_name| field_name.as_str(interns) == name_str)
.map(|idx| &self.items[idx])
}
}
impl<'h> HeapRead<'h, NamedTuple> {
#[must_use]
pub fn get_by_index<'a>(&'a self, vm: &'a VM<'h, impl ResourceTracker>, index: i64) -> Option<&'a Value> {
let len = i64::try_from(self.get(vm.heap).items.len()).ok()?;
let normalized = if index < 0 { index + len } else { index };
if normalized < 0 || normalized >= len {
return None;
}
self.get(vm.heap).items.get(usize::try_from(normalized).ok()?)
}
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)
}
#[expect(clippy::iter_not_returning_iterator)]
pub(crate) fn iter<R: ResourceTracker>(&self, vm: &mut VM<'h, R>) -> RunResult<NamedTupleIter<'_, 'h>> {
NamedTupleIter::new(self, vm)
}
pub(crate) fn eq_tuple(
&self,
other: &HeapRead<'h, super::Tuple>,
vm: &mut VM<'h, impl ResourceTracker>,
) -> RunResult<bool> {
if self.get(vm.heap).len() != other.get(vm.heap).as_slice().len() {
return Ok(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(false);
}
}
Ok(true)
}
}
pub(crate) struct NamedTupleIter<'a, 'h> {
tuple: &'a HeapRead<'h, NamedTuple>,
index: usize,
token: RecursionToken,
current: Value,
}
impl<'a, 'h> NamedTupleIter<'a, 'h> {
fn new<R: ResourceTracker>(tuple: &'a HeapRead<'h, NamedTuple>, 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 NamedTupleIter<'_, '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, NamedTuple> {
fn py_type(&self, _vm: &VM<'h, impl ResourceTracker>) -> Type {
Type::NamedTuple
}
fn py_len(&self, vm: &VM<'h, impl ResourceTracker>) -> Option<usize> {
Some(self.get(vm.heap).len())
}
fn py_getitem(&self, key: &Value, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<Value> {
let index = match key {
Value::Int(i) => *i,
_ => return Err(ExcType::type_error_indices(Type::NamedTuple, &key.py_type_name(vm))),
};
match self.get_by_index(vm, index) {
Some(value) => Ok(value.clone_with_heap(vm.heap)),
None => Err(ExcType::tuple_index_error()),
}
}
fn py_eq_impl(&self, other: &Value, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<Option<bool>> {
match other.read_heap(vm) {
Some(HeapReadOutput::NamedTuple(other)) => {
if self.get(vm.heap).len() != other.get(vm.heap).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))
}
Some(HeapReadOutput::Tuple(other)) => Ok(Some(self.eq_tuple(&other, vm)?)),
_ => Ok(None),
}
}
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_bool(&self, vm: &mut VM<'h, impl ResourceTracker>) -> bool {
self.get(vm.heap).len() > 0
}
fn py_repr_fmt(
&self,
f: &mut impl Write,
vm: &mut VM<'h, impl ResourceTracker>,
heap_ids: &mut LazyHeapSet,
) -> RunResult<()> {
let Ok(mut guard) = vm.recursion_guard() else {
return Ok(f.write_str("...")?);
};
let vm = &mut *guard;
write!(f, "{}(", self.get(vm.heap).name.as_str(vm.interns))?;
let len = self.get(vm.heap).items.len();
for i in 0..len {
if i > 0 {
f.write_str(", ")?;
}
f.write_str(self.get(vm.heap).field_names[i].as_str(vm.interns))?;
f.write_char('=')?;
let value = self.clone_item(i, vm);
defer_drop!(value, vm);
value.py_repr_fmt(f, vm, heap_ids)?;
}
f.write_char(')')?;
Ok(())
}
fn py_getattr(&self, attr: &EitherStr, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<Option<CallResult>> {
let attr_name = attr.as_str(vm.interns);
if let Some(value) = self.get(vm.heap).get_by_name(attr_name, vm.interns) {
Ok(Some(CallResult::Value(value.clone_with_heap(vm.heap))))
} else {
Err(ExcType::attribute_error(self.get(vm.heap).name(vm.interns), attr_name))
}
}
}
impl HeapItem for NamedTuple {
fn py_estimate_size(&self) -> usize {
mem::size_of::<Self>()
+ self.name.py_estimate_size()
+ self.field_names.len() * mem::size_of::<StringId>()
+ 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();
}
}
}
}