use crate::{
error::{EvalError, EvalResult, InterpreterError},
state::InterpreterState,
value::Value,
};
pub struct TypeObject {
pub name: &'static str,
pub eq_slot: EqSlot,
pub hash_slot: Option<HashSlot>,
pub lt_slot: LtSlot,
pub contains_slot: Option<ContainsSlot>,
pub arith_slot: ArithSlot,
pub iter_slot: Option<IterSlot>,
pub get_item_slot: Option<GetItemSlot>,
pub set_item_slot: Option<SetItemSlot>,
pub del_item_slot: Option<DelItemSlot>,
pub missing_slot: Option<MissingSlot>,
pub len_slot: Option<LenSlot>,
pub get_attr_slot: Option<GetAttrSlot>,
pub set_attr_slot: Option<SetAttrSlot>,
pub has_methods_table: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BinOp {
Add,
Sub,
Mul,
Div,
FloorDiv,
Mod,
Pow,
}
impl BinOp {
pub const fn symbol(self) -> &'static str {
match self {
Self::Add => "+",
Self::Sub => "-",
Self::Mul => "*",
Self::Div => "/",
Self::FloorDiv => "//",
Self::Mod => "%",
Self::Pow => "**",
}
}
}
pub type EqSlot = fn(lhs: &Value, rhs: &Value) -> Option<bool>;
pub type HashSlot = fn(value: &Value) -> Result<i64, EvalError>;
pub type LtSlot = fn(lhs: &Value, rhs: &Value) -> Option<Result<bool, EvalError>>;
pub type ContainsSlot = fn(container: &Value, item: &Value) -> Result<bool, EvalError>;
pub type ArithSlot =
fn(op: BinOp, lhs: &Value, rhs: &Value, decimal_prec: i64) -> Option<Result<Value, EvalError>>;
pub type IterSlot = fn(value: &Value) -> Result<Vec<Value>, EvalError>;
pub type GetItemSlot = fn(container: &Value, index: &Value) -> Result<Value, EvalError>;
pub type SetItemSlot =
fn(container: &mut Value, index: &Value, value: Value) -> Result<isize, EvalError>;
pub type DelItemSlot = fn(container: &mut Value, index: &Value) -> Result<isize, EvalError>;
pub type MissingSlot = fn(container: &Value, key: &Value) -> Result<Value, EvalError>;
pub type LenSlot = fn(value: &Value) -> Result<usize, EvalError>;
pub type GetAttrSlot = fn(value: &Value, name: &str) -> EvalResult;
pub type SetAttrSlot =
fn(value: &mut Value, name: &str, new_val: Value) -> Result<isize, EvalError>;
pub fn dispatch_getattr_opt(value: &Value, name: &str) -> Result<Option<Value>, EvalError> {
if is_dunder_name(name) && builtin_dunder_present(value, name) {
return Ok(Some(bound_method(value, name)));
}
if matches!(value, Value::Generator { .. } | Value::Lazy { .. } | Value::BuiltinIter { .. })
&& matches!(name, "send" | "throw" | "close")
{
return Ok(Some(bound_method(value, name)));
}
type_of(value).get_attr_slot.map_or_else(|| Ok(None), |slot| slot(value, name).map(Some))
}
fn is_dunder_name(name: &str) -> bool {
name.len() > 4 && name.starts_with("__") && name.ends_with("__")
}
const COMMON_DUNDERS: &[&str] = &[
"__delattr__",
"__dir__",
"__doc__",
"__eq__",
"__format__",
"__ge__",
"__getattribute__",
"__getstate__",
"__gt__",
"__hash__",
"__init__",
"__init_subclass__",
"__le__",
"__lt__",
"__ne__",
"__new__",
"__reduce__",
"__reduce_ex__",
"__repr__",
"__setattr__",
"__sizeof__",
"__str__",
"__subclasshook__",
];
const INT_DUNDERS: &[&str] = &[
"__abs__",
"__add__",
"__and__",
"__bool__",
"__ceil__",
"__divmod__",
"__float__",
"__floor__",
"__floordiv__",
"__getnewargs__",
"__index__",
"__int__",
"__invert__",
"__lshift__",
"__mod__",
"__mul__",
"__neg__",
"__or__",
"__pos__",
"__pow__",
"__radd__",
"__rand__",
"__rdivmod__",
"__rfloordiv__",
"__rlshift__",
"__rmod__",
"__rmul__",
"__ror__",
"__round__",
"__rpow__",
"__rrshift__",
"__rshift__",
"__rsub__",
"__rtruediv__",
"__rxor__",
"__sub__",
"__truediv__",
"__trunc__",
"__xor__",
];
const FLOAT_DUNDERS: &[&str] = &[
"__abs__",
"__add__",
"__bool__",
"__ceil__",
"__divmod__",
"__float__",
"__floor__",
"__floordiv__",
"__getformat__",
"__getnewargs__",
"__int__",
"__mod__",
"__mul__",
"__neg__",
"__pos__",
"__pow__",
"__radd__",
"__rdivmod__",
"__rfloordiv__",
"__rmod__",
"__rmul__",
"__round__",
"__rpow__",
"__rsub__",
"__rtruediv__",
"__sub__",
"__truediv__",
"__trunc__",
];
const COMPLEX_DUNDERS: &[&str] = &[
"__abs__",
"__add__",
"__bool__",
"__complex__",
"__getnewargs__",
"__mul__",
"__neg__",
"__pos__",
"__pow__",
"__radd__",
"__rmul__",
"__rpow__",
"__rsub__",
"__rtruediv__",
"__sub__",
"__truediv__",
];
const STR_DUNDERS: &[&str] = &[
"__add__",
"__contains__",
"__getitem__",
"__getnewargs__",
"__iter__",
"__len__",
"__mod__",
"__mul__",
"__rmod__",
"__rmul__",
];
const BYTES_DUNDERS: &[&str] = &[
"__add__",
"__buffer__",
"__bytes__",
"__contains__",
"__getitem__",
"__getnewargs__",
"__iter__",
"__len__",
"__mod__",
"__mul__",
"__rmod__",
"__rmul__",
];
const BYTEARRAY_DUNDERS: &[&str] = &[
"__add__",
"__alloc__",
"__buffer__",
"__contains__",
"__delitem__",
"__getitem__",
"__iadd__",
"__imul__",
"__iter__",
"__len__",
"__mod__",
"__mul__",
"__release_buffer__",
"__rmod__",
"__rmul__",
"__setitem__",
];
const LIST_DUNDERS: &[&str] = &[
"__add__",
"__class_getitem__",
"__contains__",
"__delitem__",
"__getitem__",
"__iadd__",
"__imul__",
"__iter__",
"__len__",
"__mul__",
"__reversed__",
"__rmul__",
"__setitem__",
];
const TUPLE_DUNDERS: &[&str] = &[
"__add__",
"__class_getitem__",
"__contains__",
"__getitem__",
"__getnewargs__",
"__iter__",
"__len__",
"__mul__",
"__rmul__",
];
const DICT_DUNDERS: &[&str] = &[
"__class_getitem__",
"__contains__",
"__delitem__",
"__getitem__",
"__ior__",
"__iter__",
"__len__",
"__or__",
"__reversed__",
"__ror__",
"__setitem__",
];
const SET_DUNDERS: &[&str] = &[
"__and__",
"__class_getitem__",
"__contains__",
"__iand__",
"__ior__",
"__isub__",
"__iter__",
"__ixor__",
"__len__",
"__or__",
"__rand__",
"__ror__",
"__rsub__",
"__rxor__",
"__sub__",
"__xor__",
];
const FROZENSET_DUNDERS: &[&str] = &[
"__and__",
"__class_getitem__",
"__contains__",
"__iter__",
"__len__",
"__or__",
"__rand__",
"__ror__",
"__rsub__",
"__rxor__",
"__sub__",
"__xor__",
];
const RANGE_DUNDERS: &[&str] =
&["__bool__", "__contains__", "__getitem__", "__iter__", "__len__", "__reversed__"];
const NONE_DUNDERS: &[&str] = &["__bool__"];
const ITER_DUNDERS: &[&str] = &["__iter__", "__length_hint__", "__next__", "__setstate__"];
pub(crate) fn instance_classmethod(
value: &Value,
method: &str,
) -> Option<(&'static str, &'static str)> {
match (value, method) {
(Value::Dict(_), "fromkeys") => Some(("dict", "fromkeys")),
(Value::Bytes(_), "fromhex") => Some(("bytes", "fromhex")),
(Value::Bytes(_), "maketrans") => Some(("bytes", "maketrans")),
(Value::ByteArray(_), "fromhex") => Some(("bytearray", "fromhex")),
(Value::ByteArray(_), "maketrans") => Some(("bytearray", "maketrans")),
_ => None,
}
}
pub(crate) fn builtin_dir(value: &Value) -> Option<Vec<String>> {
let (dunders, methods, data): (&[&str], &[&str], &[&str]) = match value {
Value::Int(_) | Value::BigInt(_) | Value::Bool(_) => {
(INT_DUNDERS, INT_METHODS, &["denominator", "imag", "numerator", "real"])
}
Value::Float(_) => (FLOAT_DUNDERS, FLOAT_METHODS, &["imag", "real"]),
Value::Complex(_) => (COMPLEX_DUNDERS, COMPLEX_METHODS, &["imag", "real"]),
Value::String(_) => (STR_DUNDERS, STR_METHODS, &[]),
Value::Bytes(_) => (BYTES_DUNDERS, BYTES_METHODS, &[]),
Value::ByteArray(_) => (BYTEARRAY_DUNDERS, BYTEARRAY_METHODS, &[]),
Value::List(_) => (LIST_DUNDERS, LIST_METHODS, &[]),
Value::Tuple(_) => (TUPLE_DUNDERS, TUPLE_METHODS, &[]),
Value::Dict(_) => (DICT_DUNDERS, DICT_METHODS, &[]),
Value::Set(_) => (SET_DUNDERS, SET_METHODS, &[]),
Value::Frozenset(_) => (FROZENSET_DUNDERS, FROZENSET_METHODS, &[]),
Value::Range { .. } => (RANGE_DUNDERS, RANGE_METHODS, &["start", "step", "stop"]),
Value::None => (NONE_DUNDERS, &[], &[]),
_ => return None,
};
let mut all: Vec<String> = COMMON_DUNDERS
.iter()
.chain(dunders)
.chain(methods)
.chain(data)
.copied()
.chain(std::iter::once("__class__"))
.map(str::to_string)
.collect();
all.sort_unstable();
all.dedup();
Some(all)
}
pub(crate) fn builtin_dunder_present(value: &Value, name: &str) -> bool {
let extras: &[&str] = match value {
Value::Int(_) | Value::BigInt(_) | Value::Bool(_) => INT_DUNDERS,
Value::Float(_) => FLOAT_DUNDERS,
Value::Complex(_) => COMPLEX_DUNDERS,
Value::String(_) => STR_DUNDERS,
Value::Bytes(_) => BYTES_DUNDERS,
Value::ByteArray(_) => BYTEARRAY_DUNDERS,
Value::List(_) => LIST_DUNDERS,
Value::Tuple(_) => TUPLE_DUNDERS,
Value::Dict(_) => DICT_DUNDERS,
Value::Set(_) => SET_DUNDERS,
Value::Frozenset(_) => FROZENSET_DUNDERS,
Value::Range { .. } => RANGE_DUNDERS,
Value::None => NONE_DUNDERS,
Value::Lazy { .. } | Value::Generator { .. } | Value::BuiltinIter { .. } => ITER_DUNDERS,
_ => return false,
};
COMMON_DUNDERS.contains(&name) || extras.contains(&name)
}
pub(crate) fn builtin_type_attr_present(type_name: &str, attr: &str) -> bool {
if matches!(attr, "__name__" | "__qualname__" | "__call__") {
return true;
}
let (dunders, methods, data): (&[&str], &[&str], &[&str]) = match type_name {
"int" | "bool" => (INT_DUNDERS, INT_METHODS, &["denominator", "imag", "numerator", "real"]),
"float" => (FLOAT_DUNDERS, FLOAT_METHODS, &["imag", "real"]),
"complex" => (COMPLEX_DUNDERS, COMPLEX_METHODS, &["imag", "real"]),
"str" => (STR_DUNDERS, STR_METHODS, &[]),
"bytes" => (BYTES_DUNDERS, BYTES_METHODS, &[]),
"bytearray" => (BYTEARRAY_DUNDERS, BYTEARRAY_METHODS, &[]),
"list" => (LIST_DUNDERS, LIST_METHODS, &[]),
"tuple" => (TUPLE_DUNDERS, TUPLE_METHODS, &[]),
"dict" => (DICT_DUNDERS, DICT_METHODS, &[]),
"set" => (SET_DUNDERS, SET_METHODS, &[]),
"frozenset" => (FROZENSET_DUNDERS, FROZENSET_METHODS, &[]),
"range" => (RANGE_DUNDERS, RANGE_METHODS, &["start", "step", "stop"]),
"NoneType" => (NONE_DUNDERS, &[], &[]),
"object" => (&[], &[], &[]),
_ => return false,
};
COMMON_DUNDERS.contains(&attr)
|| dunders.contains(&attr)
|| methods.contains(&attr)
|| data.contains(&attr)
}
pub fn dispatch_setattr(value: &mut Value, name: &str, new_val: Value) -> Result<isize, EvalError> {
let type_obj = type_of(value);
if let Some(slot) = type_obj.set_attr_slot {
return slot(value, name, new_val);
}
Err(InterpreterError::AttributeError(format!(
"'{}' object has no attribute '{name}'",
type_obj.name
))
.into())
}
pub fn dispatch_getitem(container: &Value, index: &Value) -> Result<Value, EvalError> {
if let Value::Array { typecode, items } = container {
let result = dispatch_getitem(&Value::List(items.clone()), index)?;
return Ok(match result {
Value::List(l) => Value::Array { typecode: *typecode, items: l },
elem => elem,
});
}
let container_type = type_of(container);
container_type.get_item_slot.map_or_else(
|| {
Err(InterpreterError::TypeError(format!(
"'{}' object is not subscriptable",
container_type.name
))
.into())
},
|slot| slot(container, index),
)
}
pub fn dispatch_setitem(
container: &mut Value,
index: &Value,
value: Value,
) -> Result<isize, EvalError> {
if let Value::Array { items, .. } = container {
let mut list = Value::List(items.clone());
return dispatch_setitem(&mut list, index, value);
}
let container_type = type_of(container);
if let Some(slot) = container_type.set_item_slot {
return slot(container, index, value);
}
Err(InterpreterError::TypeError(format!(
"'{}' object does not support item assignment",
container_type.name
))
.into())
}
pub fn dispatch_delitem(container: &mut Value, index: &Value) -> Result<isize, EvalError> {
let container_type = type_of(container);
if let Some(slot) = container_type.del_item_slot {
return slot(container, index);
}
Err(InterpreterError::TypeError(format!(
"'{}' object does not support item deletion",
container_type.name
))
.into())
}
pub fn dispatch_len(value: &Value) -> Result<usize, EvalError> {
if let Value::Array { items, .. } = value {
return Ok(items.lock().len());
}
let type_obj = type_of(value);
type_obj.len_slot.map_or_else(
|| {
Err(InterpreterError::TypeError(format!(
"object of type '{}' has no len()",
type_obj.name
))
.into())
},
|slot| slot(value),
)
}
pub fn dispatch_iter(value: &Value) -> Result<Vec<Value>, EvalError> {
if let Value::Lazy { items, .. } = value {
return Ok(items.clone());
}
if let Value::Array { items, .. } = value {
return Ok(items.lock().clone());
}
let type_obj = type_of(value);
type_obj.iter_slot.map_or_else(
|| {
Err(InterpreterError::TypeError(format!("'{}' object is not iterable", type_obj.name))
.into())
},
|slot| slot(value),
)
}
pub fn dispatch_binop(
op: BinOp,
lhs: &Value,
rhs: &Value,
decimal_prec: i64,
) -> Result<Value, EvalError> {
let lhs_u = unwrap_enum_for_compare(lhs);
let rhs_u = unwrap_enum_for_compare(rhs);
if !std::ptr::eq(lhs_u, lhs) || !std::ptr::eq(rhs_u, rhs) {
return dispatch_binop(op, lhs_u, rhs_u, decimal_prec);
}
if matches!(lhs, Value::Array { .. }) {
if let Some(result) = array_arith(op, lhs, rhs) {
return result;
}
}
let lhs_type = type_of(lhs);
if let Some(result) = (lhs_type.arith_slot)(op, lhs, rhs, decimal_prec) {
return result;
}
let rhs_type = type_of(rhs);
if let Some(result) = (rhs_type.arith_slot)(op, lhs, rhs, decimal_prec) {
return result;
}
if matches!(op, BinOp::Add) {
match lhs {
Value::String(_) | Value::List(_) | Value::Tuple(_) => {
return Err(InterpreterError::TypeError(format!(
"can only concatenate {0} (not \"{1}\") to {0}",
lhs_type.name,
rhs.python_type_name(),
))
.into());
}
Value::Bytes(_) | Value::ByteArray(_) => {
return Err(InterpreterError::TypeError(format!(
"can't concat {} to {}",
rhs.python_type_name(),
lhs_type.name,
))
.into());
}
_ => {}
}
}
if matches!(op, BinOp::Mul) {
let is_seq = |v: &Value| {
matches!(
v,
Value::String(_)
| Value::List(_)
| Value::Tuple(_)
| Value::Bytes(_)
| Value::ByteArray(_)
)
};
if is_seq(lhs) {
return Err(InterpreterError::TypeError(format!(
"can't multiply sequence by non-int of type '{}'",
rhs.python_type_name(),
))
.into());
}
if is_seq(rhs) {
return Err(InterpreterError::TypeError(format!(
"can't multiply sequence by non-int of type '{}'",
lhs.python_type_name(),
))
.into());
}
}
Err(InterpreterError::TypeError(format!(
"unsupported operand type(s) for {}: '{}' and '{}'",
op.symbol(),
lhs.python_type_name(),
rhs.python_type_name(),
))
.into())
}
pub fn dispatch_lt(lhs: &Value, rhs: &Value) -> Result<bool, EvalError> {
let lhs_u = unwrap_enum_for_compare(lhs);
let rhs_u = unwrap_enum_for_compare(rhs);
if !std::ptr::eq(lhs_u, lhs) || !std::ptr::eq(rhs_u, rhs) {
return dispatch_lt(lhs_u, rhs_u);
}
let lhs_type = type_of(lhs);
if let Some(result) = (lhs_type.lt_slot)(lhs, rhs) {
return result;
}
let rhs_type = type_of(rhs);
if let Some(result) = (rhs_type.lt_slot)(lhs, rhs) {
return result;
}
Err(type_error_unsupported("<", lhs, rhs))
}
fn unwrap_enum_for_compare(value: &Value) -> &Value {
match value {
Value::EnumMember {
value: inner,
kind: crate::value::EnumKind::Int | crate::value::EnumKind::Str,
..
} => inner.as_ref(),
_ => value,
}
}
pub fn dispatch_contains(container: &Value, item: &Value) -> Result<bool, EvalError> {
if let Value::Array { items, .. } = container {
return dispatch_contains(&Value::List(items.clone()), item);
}
let container_type = type_of(container);
container_type.contains_slot.map_or_else(
|| {
Err(InterpreterError::TypeError(format!(
"argument of type '{}' is not iterable",
container_type.name
))
.into())
},
|slot| slot(container, item),
)
}
pub fn dispatch_hash(state: &InterpreterState, value: &Value) -> Result<i64, EvalError> {
if let Value::EnumMember {
value: inner,
kind:
crate::value::EnumKind::Int | crate::value::EnumKind::IntFlag | crate::value::EnumKind::Str,
..
} = value
{
return dispatch_hash(state, inner);
}
if let Value::Instance(inst) = value {
let registered = state.classes.get(&inst.class_name);
let defines = |dunder: &str| {
registered.is_some_and(|class| {
class.mro.iter().any(|anc| {
state.classes.get(anc).is_some_and(|c| c.methods.contains_key(dunder))
})
})
};
let hash_is_none = registered.is_some_and(|class| {
class.mro.iter().find_map(|anc| {
state.classes.get(anc).and_then(|c| {
if c.methods.contains_key("__hash__") {
Some(false)
} else if matches!(c.class_attrs.get("__hash__"), Some(Value::None)) {
Some(true)
} else {
None
}
})
}) == Some(true)
});
let has_hash = defines("__hash__");
let dataclass_default =
registered.is_some_and(|c| c.dataclass_fields.is_some()) && !has_hash;
if hash_is_none || dataclass_default || (defines("__eq__") && !has_hash) {
return Err(InterpreterError::TypeError(format!(
"unhashable type: '{}'",
inst.class_name
))
.into());
}
if !has_hash {
use std::sync::Arc;
return Ok(finalize_hash(Arc::as_ptr(&inst.fields).addr() as i64));
}
}
let type_obj = type_of(value);
type_obj.hash_slot.map_or_else(
|| Err(InterpreterError::TypeError(format!("unhashable type: '{}'", type_obj.name)).into()),
|slot| slot(value),
)
}
fn namedtuple_field_values(
state: &InterpreterState,
inst: &crate::value::InstanceValue,
) -> Option<Vec<Value>> {
let class = state.classes.get(&inst.class_name)?;
let Value::Tuple(field_names) = class.class_attrs.get("_fields")? else {
return None;
};
let fields = inst.fields.lock();
Some(
field_names
.iter()
.map(|name| match name {
Value::String(n) => fields.get(n.as_str()).cloned().unwrap_or(Value::None),
_ => Value::None,
})
.collect(),
)
}
pub fn dispatch_eq(state: &InterpreterState, lhs: &Value, rhs: &Value) -> EvalResult {
if let Value::Instance(inst) = lhs {
if let Value::Instance(other_inst) = rhs {
if inst.class_name == other_inst.class_name {
if let Some(class) = state.classes.get(&inst.class_name) {
if let Some(fields) = &class.dataclass_fields {
if !class.methods.contains_key("__eq__") {
if std::sync::Arc::ptr_eq(&inst.fields, &other_inst.fields) {
return Ok(Value::Bool(true));
}
let mut equal = true;
let af = inst.fields.lock();
let bf = other_inst.fields.lock();
for field in fields.iter().filter(|f| f.compare && !f.init_only) {
match (af.get(&field.name), bf.get(&field.name)) {
(Some(a), Some(b)) => {
let cmp = dispatch_eq(state, a, b)?;
if !matches!(cmp, Value::Bool(true)) {
equal = false;
break;
}
}
(None, None) => {}
_ => {
equal = false;
break;
}
}
}
return Ok(Value::Bool(equal));
}
}
}
}
}
if let Some(lhs_fields) = namedtuple_field_values(state, inst) {
let rhs_elems = match rhs {
Value::Tuple(items) => Some(items.clone()),
Value::Instance(other) => namedtuple_field_values(state, other),
_ => None,
};
if let Some(rhs_elems) = rhs_elems {
if lhs_fields.len() != rhs_elems.len() {
return Ok(Value::Bool(false));
}
for (a, b) in lhs_fields.iter().zip(&rhs_elems) {
if !matches!(dispatch_eq(state, a, b)?, Value::Bool(true)) {
return Ok(Value::Bool(false));
}
}
return Ok(Value::Bool(true));
}
return Ok(Value::Bool(false));
}
return Ok(Value::Bool(matches!(
rhs,
Value::Instance(other_inst) if std::sync::Arc::ptr_eq(&inst.fields, &other_inst.fields)
)));
}
let lhs_type = type_of(lhs);
if let Some(result) = (lhs_type.eq_slot)(lhs, rhs) {
return Ok(Value::Bool(result));
}
let rhs_type = type_of(rhs);
if let Some(result) = (rhs_type.eq_slot)(rhs, lhs) {
return Ok(Value::Bool(result));
}
Ok(Value::Bool(false))
}
fn type_of(value: &Value) -> &'static TypeObject {
match value {
Value::None => &NONE_TYPE,
Value::Bool(_) => &BOOL_TYPE,
Value::Int(_) | Value::BigInt(_) => &INT_TYPE,
Value::Float(_) => &FLOAT_TYPE,
Value::Complex(_) => &COMPLEX_TYPE,
Value::String(_) => &STR_TYPE,
Value::Bytes(_) => &BYTES_TYPE,
Value::ByteArray(_) => &BYTEARRAY_TYPE,
Value::MemoryView(_) => &MEMORYVIEW_TYPE,
Value::List(_) => &LIST_TYPE,
Value::Tuple(_) => &TUPLE_TYPE,
Value::Dict(_) => &DICT_TYPE,
Value::OrderedDict(_) => &ORDEREDDICT_TYPE,
Value::Set(_) => &SET_TYPE,
Value::Frozenset(_) => &FROZENSET_TYPE,
Value::Range { .. } => &RANGE_TYPE,
Value::Counter(_) => &COUNTER_TYPE,
Value::Deque { .. } => &DEQUE_TYPE,
Value::DefaultDict { .. } => &DEFAULTDICT_TYPE,
Value::ChainMap(_) => &CHAINMAP_TYPE,
Value::DictView { .. } => &DICTVIEW_TYPE,
Value::Decimal(..) => &DECIMAL_TYPE,
Value::Fraction(_) => &FRACTION_TYPE,
Value::Date(_) => &DATE_TYPE,
Value::DateTime { .. } => &DATETIME_TYPE,
Value::Time(_) => &TIME_TYPE,
Value::TimeDelta(_) => &TIMEDELTA_TYPE,
Value::TimeZone(_) => &TIMEZONE_TYPE,
Value::HashDigest { .. } => &HASHDIGEST_TYPE,
Value::EnumMember { .. } => &ENUMMEMBER_TYPE,
_ => &OBJECT_TYPE,
}
}
#[must_use]
pub fn type_name_of(value: &Value) -> &'static str {
type_of(value).name
}
#[must_use]
pub fn type_has_methods_table(value: &Value) -> bool {
type_of(value).has_methods_table
}
static NONE_TYPE: TypeObject = TypeObject {
name: "NoneType",
eq_slot: none_eq,
hash_slot: Some(none_hash),
lt_slot: noimpl_lt,
contains_slot: None,
arith_slot: noimpl_arith,
iter_slot: None,
get_item_slot: None,
set_item_slot: None,
del_item_slot: None,
missing_slot: None,
len_slot: None,
get_attr_slot: Some(noattr_get_attr),
set_attr_slot: None,
has_methods_table: false,
};
static BOOL_TYPE: TypeObject = TypeObject {
name: "bool",
eq_slot: bool_eq,
hash_slot: Some(bool_hash),
lt_slot: bool_lt,
contains_slot: None,
arith_slot: numeric_arith,
iter_slot: None,
get_item_slot: None,
set_item_slot: None,
del_item_slot: None,
missing_slot: None,
len_slot: None,
get_attr_slot: Some(bool_get_attr),
set_attr_slot: None,
has_methods_table: false,
};
static INT_TYPE: TypeObject = TypeObject {
name: "int",
eq_slot: int_eq,
hash_slot: Some(int_hash_slot),
lt_slot: int_lt,
contains_slot: None,
arith_slot: numeric_arith,
iter_slot: None,
get_item_slot: None,
set_item_slot: None,
del_item_slot: None,
missing_slot: None,
len_slot: None,
get_attr_slot: Some(int_get_attr),
set_attr_slot: None,
has_methods_table: true,
};
static FLOAT_TYPE: TypeObject = TypeObject {
name: "float",
eq_slot: float_eq,
hash_slot: Some(float_hash_slot),
lt_slot: float_lt,
contains_slot: None,
arith_slot: numeric_arith,
iter_slot: None,
get_item_slot: None,
set_item_slot: None,
del_item_slot: None,
missing_slot: None,
len_slot: None,
get_attr_slot: Some(float_get_attr),
set_attr_slot: None,
has_methods_table: true,
};
static COMPLEX_TYPE: TypeObject = TypeObject {
name: "complex",
eq_slot: complex_eq,
hash_slot: Some(complex_hash_slot),
lt_slot: noimpl_lt,
contains_slot: None,
arith_slot: complex_arith,
iter_slot: None,
get_item_slot: None,
set_item_slot: None,
del_item_slot: None,
missing_slot: None,
len_slot: None,
get_attr_slot: Some(complex_get_attr),
set_attr_slot: None,
has_methods_table: true,
};
static STR_TYPE: TypeObject = TypeObject {
name: "str",
eq_slot: str_eq,
hash_slot: Some(fallback_hash_slot),
lt_slot: str_lt,
contains_slot: Some(str_contains),
arith_slot: str_arith,
iter_slot: Some(str_iter),
get_item_slot: Some(str_get_item),
set_item_slot: None,
del_item_slot: None,
missing_slot: None,
len_slot: Some(str_len),
get_attr_slot: Some(str_get_attr),
set_attr_slot: None,
has_methods_table: true,
};
static BYTES_TYPE: TypeObject = TypeObject {
name: "bytes",
eq_slot: bytes_eq,
hash_slot: Some(fallback_hash_slot),
lt_slot: bytes_lt,
contains_slot: Some(bytes_contains),
arith_slot: bytes_arith,
iter_slot: Some(bytes_iter),
get_item_slot: Some(bytes_get_item),
set_item_slot: None,
del_item_slot: None,
missing_slot: None,
len_slot: Some(bytes_len),
get_attr_slot: Some(bytes_get_attr),
set_attr_slot: None,
has_methods_table: true,
};
static BYTEARRAY_TYPE: TypeObject = TypeObject {
name: "bytearray",
eq_slot: bytes_eq,
hash_slot: None,
lt_slot: bytes_lt,
contains_slot: Some(bytes_contains),
arith_slot: bytes_arith,
iter_slot: Some(bytes_iter),
get_item_slot: Some(bytes_get_item),
set_item_slot: Some(bytearray_set_item),
del_item_slot: Some(bytearray_del_item),
missing_slot: None,
len_slot: Some(bytes_len),
get_attr_slot: Some(bytearray_get_attr),
set_attr_slot: None,
has_methods_table: true,
};
static MEMORYVIEW_TYPE: TypeObject = TypeObject {
name: "memoryview",
eq_slot: bytes_eq,
hash_slot: Some(fallback_hash_slot),
lt_slot: bytes_lt,
contains_slot: Some(bytes_contains),
arith_slot: noimpl_arith,
iter_slot: Some(bytes_iter),
get_item_slot: Some(bytes_get_item),
set_item_slot: None,
del_item_slot: None,
missing_slot: None,
len_slot: Some(bytes_len),
get_attr_slot: Some(memoryview_get_attr),
set_attr_slot: None,
has_methods_table: true,
};
static LIST_TYPE: TypeObject = TypeObject {
name: "list",
eq_slot: list_eq,
hash_slot: None,
lt_slot: list_lt,
contains_slot: Some(sequence_contains),
arith_slot: list_arith,
iter_slot: Some(sequence_iter),
get_item_slot: Some(sequence_get_item),
set_item_slot: Some(list_set_item),
del_item_slot: Some(list_del_item),
missing_slot: None,
len_slot: Some(sequence_len),
get_attr_slot: Some(list_get_attr),
set_attr_slot: None,
has_methods_table: true,
};
static TUPLE_TYPE: TypeObject = TypeObject {
name: "tuple",
eq_slot: tuple_eq,
hash_slot: Some(fallback_hash_slot),
lt_slot: tuple_lt,
contains_slot: Some(sequence_contains),
arith_slot: tuple_arith,
iter_slot: Some(sequence_iter),
get_item_slot: Some(sequence_get_item),
set_item_slot: None,
del_item_slot: None,
missing_slot: None,
len_slot: Some(sequence_len),
get_attr_slot: Some(tuple_get_attr),
set_attr_slot: None,
has_methods_table: true,
};
static DICT_TYPE: TypeObject = TypeObject {
name: "dict",
eq_slot: dict_eq,
hash_slot: None,
lt_slot: noimpl_lt,
contains_slot: Some(dict_contains),
arith_slot: noimpl_arith,
iter_slot: Some(dict_iter),
get_item_slot: Some(dict_get_item),
set_item_slot: Some(dict_set_item),
del_item_slot: Some(dict_del_item),
missing_slot: None,
len_slot: Some(dict_len),
get_attr_slot: Some(dict_get_attr),
set_attr_slot: None,
has_methods_table: true,
};
static ORDEREDDICT_TYPE: TypeObject = TypeObject {
name: "OrderedDict",
eq_slot: ordered_dict_eq,
hash_slot: None,
lt_slot: noimpl_lt,
contains_slot: Some(dict_contains),
arith_slot: noimpl_arith,
iter_slot: Some(dict_iter),
get_item_slot: Some(dict_get_item),
set_item_slot: Some(dict_set_item),
del_item_slot: Some(dict_del_item),
missing_slot: None,
len_slot: Some(dict_len),
get_attr_slot: Some(dict_get_attr),
set_attr_slot: None,
has_methods_table: true,
};
static SET_TYPE: TypeObject = TypeObject {
name: "set",
eq_slot: set_eq,
hash_slot: None,
lt_slot: set_lt,
contains_slot: Some(sequence_contains),
arith_slot: set_arith,
iter_slot: Some(set_iter),
get_item_slot: None,
set_item_slot: None,
del_item_slot: None,
missing_slot: None,
len_slot: Some(sequence_len),
get_attr_slot: Some(set_get_attr),
set_attr_slot: None,
has_methods_table: true,
};
static FROZENSET_TYPE: TypeObject = TypeObject {
name: "frozenset",
eq_slot: set_eq,
hash_slot: Some(fallback_hash_slot),
lt_slot: set_lt,
contains_slot: Some(sequence_contains),
arith_slot: set_arith,
iter_slot: Some(set_iter),
get_item_slot: None,
set_item_slot: None,
del_item_slot: None,
missing_slot: None,
len_slot: Some(sequence_len),
get_attr_slot: Some(frozenset_get_attr),
set_attr_slot: None,
has_methods_table: true,
};
static RANGE_TYPE: TypeObject = TypeObject {
name: "range",
eq_slot: object_eq,
hash_slot: Some(fallback_hash_slot),
lt_slot: noimpl_lt,
contains_slot: Some(range_contains),
arith_slot: noimpl_arith,
iter_slot: Some(range_iter),
get_item_slot: Some(range_get_item),
set_item_slot: None,
del_item_slot: None,
missing_slot: None,
len_slot: Some(range_len),
get_attr_slot: Some(range_get_attr),
set_attr_slot: None,
has_methods_table: false,
};
static COUNTER_TYPE: TypeObject = TypeObject {
name: "Counter",
eq_slot: counter_eq,
hash_slot: None,
lt_slot: noimpl_lt,
contains_slot: Some(counter_contains),
arith_slot: counter_arith,
iter_slot: Some(counter_iter),
get_item_slot: Some(counter_get_item),
set_item_slot: Some(counter_set_item),
del_item_slot: Some(counter_del_item),
missing_slot: Some(counter_missing),
len_slot: Some(counter_len),
get_attr_slot: Some(counter_get_attr),
set_attr_slot: None,
has_methods_table: true,
};
static DEQUE_TYPE: TypeObject = TypeObject {
name: "deque",
eq_slot: deque_eq,
hash_slot: None,
lt_slot: noimpl_lt,
contains_slot: Some(deque_contains),
arith_slot: noimpl_arith,
iter_slot: Some(deque_iter),
get_item_slot: Some(deque_get_item),
set_item_slot: Some(deque_set_item),
del_item_slot: Some(deque_del_item),
missing_slot: None,
len_slot: Some(deque_len),
get_attr_slot: Some(deque_get_attr),
set_attr_slot: None,
has_methods_table: true,
};
static DEFAULTDICT_TYPE: TypeObject = TypeObject {
name: "defaultdict",
eq_slot: noimpl_eq,
hash_slot: None,
lt_slot: noimpl_lt,
contains_slot: Some(defaultdict_contains),
arith_slot: noimpl_arith,
iter_slot: Some(defaultdict_iter),
get_item_slot: None,
set_item_slot: Some(defaultdict_set_item),
del_item_slot: Some(defaultdict_del_item),
missing_slot: None,
len_slot: Some(defaultdict_len),
get_attr_slot: Some(dict_get_attr),
set_attr_slot: None,
has_methods_table: true,
};
static DICTVIEW_TYPE: TypeObject = TypeObject {
name: "dict_view",
eq_slot: dictview_eq,
hash_slot: None,
lt_slot: dictview_lt,
contains_slot: Some(dictview_contains),
arith_slot: noimpl_arith,
iter_slot: Some(dictview_iter),
get_item_slot: None,
set_item_slot: None,
del_item_slot: None,
missing_slot: None,
len_slot: Some(dictview_len),
get_attr_slot: None,
set_attr_slot: None,
has_methods_table: true,
};
#[expect(clippy::unnecessary_wraps, reason = "IterSlot protocol")]
fn dictview_iter(value: &Value) -> Result<Vec<Value>, EvalError> {
let Value::DictView { dict, kind } = value else {
unreachable!("dictview_iter only on DICTVIEW_TYPE")
};
let guard = dict.lock();
Ok(match kind {
crate::value::DictViewKind::Keys => {
guard.keys().map(crate::value::ValueKey::to_value).collect()
}
crate::value::DictViewKind::Values => guard.values().cloned().collect(),
crate::value::DictViewKind::Items => {
guard.iter().map(|(k, v)| Value::Tuple(vec![k.to_value(), v.clone()])).collect()
}
})
}
#[expect(clippy::unnecessary_wraps, reason = "LenSlot protocol")]
fn dictview_len(value: &Value) -> Result<usize, EvalError> {
let Value::DictView { dict, .. } = value else {
unreachable!("dictview_len only on DICTVIEW_TYPE")
};
let len = dict.lock().len();
Ok(len)
}
fn dictview_contains(container: &Value, item: &Value) -> Result<bool, EvalError> {
let Value::DictView { dict, kind } = container else {
unreachable!("dictview_contains only on DICTVIEW_TYPE")
};
let guard = dict.lock();
Ok(match kind {
crate::value::DictViewKind::Keys => {
crate::eval::literals::value_to_key(item).is_ok_and(|k| guard.contains_key(&k))
}
crate::value::DictViewKind::Values => {
guard.values().any(|v| crate::eval::operations::values_equal_pub(v, item))
}
crate::value::DictViewKind::Items => match item {
Value::Tuple(pair) if pair.len() == 2 => crate::eval::literals::value_to_key(&pair[0])
.ok()
.and_then(|k| guard.get(&k))
.is_some_and(|v| crate::eval::operations::values_equal_pub(v, &pair[1])),
_ => false,
},
})
}
fn dictview_eq(lhs: &Value, rhs: &Value) -> Option<bool> {
let to_elems = |v: &Value| -> Option<Vec<Value>> {
match v {
Value::DictView { .. } => dictview_iter(v).ok(),
_ => v.set_items(),
}
};
let (a, b) = (to_elems(lhs)?, to_elems(rhs)?);
Some(a.len() == b.len() && a.iter().all(|x| b.iter().any(|y| recurse_eq(x, y))))
}
fn dictview_lt(lhs: &Value, rhs: &Value) -> Option<Result<bool, EvalError>> {
let to_elems = |v: &Value| -> Option<Vec<Value>> {
match v {
Value::DictView { .. } => dictview_iter(v).ok(),
_ => v.set_items(),
}
};
let (a, b) = (to_elems(lhs)?, to_elems(rhs)?);
let is_proper = a.len() < b.len() && a.iter().all(|av| b.iter().any(|bv| recurse_eq(av, bv)));
Some(Ok(is_proper))
}
static CHAINMAP_TYPE: TypeObject = TypeObject {
name: "ChainMap",
eq_slot: noimpl_eq,
hash_slot: None,
lt_slot: noimpl_lt,
contains_slot: Some(chainmap_contains),
arith_slot: noimpl_arith,
iter_slot: Some(chainmap_iter),
get_item_slot: Some(chainmap_get_item),
set_item_slot: Some(chainmap_set_item),
del_item_slot: Some(chainmap_del_item),
missing_slot: None,
len_slot: Some(chainmap_len),
get_attr_slot: Some(chainmap_get_attr),
set_attr_slot: None,
has_methods_table: true,
};
fn chainmap_for_each_map(
maps: &[Value],
mut f: impl FnMut(&indexmap::IndexMap<crate::value::ValueKey, Value>),
) {
for m in maps {
if let Value::Dict(map) = m {
f(&map.lock());
}
}
}
pub(crate) fn chainmap_contents(
maps: &[Value],
) -> indexmap::IndexMap<crate::value::ValueKey, Value> {
let mut out = indexmap::IndexMap::new();
for m in maps.iter().rev() {
if let Value::Dict(map) = m {
for (k, v) in map.lock().iter() {
out.insert(k.clone(), v.clone());
}
}
}
out
}
fn chainmap_get_item(container: &Value, index: &Value) -> Result<Value, EvalError> {
let Value::ChainMap(maps) = container else {
unreachable!("chainmap_get_item only on CHAINMAP_TYPE")
};
let key = crate::eval::literals::value_to_key(index)?;
for m in maps {
if let Value::Dict(map) = m {
if let Some(v) = map.lock().get(&key).cloned() {
return Ok(v);
}
}
}
Err(crate::value::ExceptionValue::key_error(&key).into())
}
fn chainmap_set_item(
container: &mut Value,
index: &Value,
value: Value,
) -> Result<isize, EvalError> {
let Value::ChainMap(maps) = container else {
unreachable!("chainmap_set_item only on CHAINMAP_TYPE")
};
let key = crate::eval::literals::value_to_key(index)?;
if let Some(Value::Dict(first)) = maps.first() {
first.lock().insert(key, value);
}
Ok(0)
}
fn chainmap_del_item(container: &mut Value, index: &Value) -> Result<isize, EvalError> {
let Value::ChainMap(maps) = container else {
unreachable!("chainmap_del_item only on CHAINMAP_TYPE")
};
let key = crate::eval::literals::value_to_key(index)?;
if let Some(Value::Dict(first)) = maps.first() {
if first.lock().shift_remove(&key).is_some() {
return Ok(0);
}
}
Err(crate::value::ExceptionValue::new(
"KeyError",
format!("Key not found in the first mapping: {key}"),
)
.into())
}
fn chainmap_contains(container: &Value, item: &Value) -> Result<bool, EvalError> {
let Value::ChainMap(maps) = container else {
unreachable!("chainmap_contains only on CHAINMAP_TYPE")
};
let key = crate::eval::literals::value_to_key(item)?;
let mut found = false;
chainmap_for_each_map(maps, |m| found = found || m.contains_key(&key));
Ok(found)
}
#[expect(clippy::unnecessary_wraps, reason = "LenSlot protocol")]
fn chainmap_len(value: &Value) -> Result<usize, EvalError> {
let Value::ChainMap(maps) = value else { unreachable!("chainmap_len only on CHAINMAP_TYPE") };
#[expect(
clippy::mutable_key_type,
reason = "ValueKey's interior mutability is not used for its Hash/Eq (keys are hashable \
ValueKey variants), so it is a sound HashSet key"
)]
let mut seen: rustc_hash::FxHashSet<crate::value::ValueKey> = rustc_hash::FxHashSet::default();
chainmap_for_each_map(maps, |m| {
for k in m.keys() {
seen.insert(k.clone());
}
});
Ok(seen.len())
}
#[expect(clippy::unnecessary_wraps, reason = "IterSlot protocol")]
fn chainmap_iter(value: &Value) -> Result<Vec<Value>, EvalError> {
let Value::ChainMap(maps) = value else { unreachable!("chainmap_iter only on CHAINMAP_TYPE") };
#[expect(
clippy::mutable_key_type,
reason = "ValueKey's interior mutability is not used for its Hash/Eq (keys are hashable \
ValueKey variants), so it is a sound HashSet key"
)]
let mut seen: rustc_hash::FxHashSet<crate::value::ValueKey> = rustc_hash::FxHashSet::default();
let mut order: Vec<crate::value::ValueKey> = Vec::new();
for m in maps.iter().rev() {
if let Value::Dict(map) = m {
for k in map.lock().keys() {
if seen.insert(k.clone()) {
order.push(k.clone());
}
}
}
}
Ok(order.into_iter().map(|k| k.to_value()).collect())
}
fn chainmap_get_attr(value: &Value, attr: &str) -> Result<Value, EvalError> {
let Value::ChainMap(maps) = value else {
unreachable!("chainmap_get_attr only on CHAINMAP_TYPE")
};
match attr {
"maps" => Ok(Value::List(crate::value::shared_list(maps.clone()))),
"parents" => {
let rest: Vec<Value> = maps.iter().skip(1).cloned().collect();
let rest = if rest.is_empty() {
vec![Value::Dict(crate::value::shared_dict(indexmap::IndexMap::new()))]
} else {
rest
};
Ok(Value::ChainMap(rest))
}
_ => Err(InterpreterError::AttributeError(format!(
"'ChainMap' object has no attribute '{attr}'"
))
.into()),
}
}
static HASHDIGEST_TYPE: TypeObject = TypeObject {
name: "_hashlib.HASH",
eq_slot: object_eq,
hash_slot: Some(fallback_hash_slot),
lt_slot: noimpl_lt,
contains_slot: None,
arith_slot: noimpl_arith,
iter_slot: None,
get_item_slot: None,
set_item_slot: None,
del_item_slot: None,
missing_slot: None,
len_slot: None,
get_attr_slot: Some(hashdigest_get_attr),
set_attr_slot: None,
has_methods_table: true,
};
static ENUMMEMBER_TYPE: TypeObject = TypeObject {
name: "enum",
eq_slot: object_eq,
hash_slot: Some(fallback_hash_slot),
lt_slot: noimpl_lt,
contains_slot: Some(enummember_contains),
arith_slot: noimpl_arith,
iter_slot: None,
get_item_slot: None,
set_item_slot: None,
del_item_slot: None,
missing_slot: None,
len_slot: None,
get_attr_slot: Some(enummember_get_attr),
set_attr_slot: None,
has_methods_table: false,
};
static DATE_TYPE: TypeObject = TypeObject {
name: "date",
eq_slot: object_eq,
hash_slot: Some(fallback_hash_slot),
lt_slot: date_lt,
contains_slot: None,
arith_slot: datetime_cluster_arith,
iter_slot: None,
get_item_slot: None,
set_item_slot: None,
del_item_slot: None,
missing_slot: None,
len_slot: None,
get_attr_slot: Some(date_get_attr),
set_attr_slot: None,
has_methods_table: true,
};
static DATETIME_TYPE: TypeObject = TypeObject {
name: "datetime",
eq_slot: object_eq,
hash_slot: Some(fallback_hash_slot),
lt_slot: datetime_lt,
contains_slot: None,
arith_slot: datetime_cluster_arith,
iter_slot: None,
get_item_slot: None,
set_item_slot: None,
del_item_slot: None,
missing_slot: None,
len_slot: None,
get_attr_slot: Some(datetime_get_attr),
set_attr_slot: None,
has_methods_table: true,
};
static TIME_TYPE: TypeObject = TypeObject {
name: "time",
eq_slot: object_eq,
hash_slot: Some(fallback_hash_slot),
lt_slot: time_lt,
contains_slot: None,
arith_slot: noimpl_arith,
iter_slot: None,
get_item_slot: None,
set_item_slot: None,
del_item_slot: None,
missing_slot: None,
len_slot: None,
get_attr_slot: Some(time_get_attr),
set_attr_slot: None,
has_methods_table: true,
};
static TIMEDELTA_TYPE: TypeObject = TypeObject {
name: "timedelta",
eq_slot: object_eq,
hash_slot: Some(fallback_hash_slot),
lt_slot: timedelta_lt,
contains_slot: None,
arith_slot: datetime_cluster_arith,
iter_slot: None,
get_item_slot: None,
set_item_slot: None,
del_item_slot: None,
missing_slot: None,
len_slot: None,
get_attr_slot: Some(timedelta_get_attr),
set_attr_slot: None,
has_methods_table: true,
};
static TIMEZONE_TYPE: TypeObject = TypeObject {
name: "timezone",
eq_slot: object_eq,
hash_slot: Some(fallback_hash_slot),
lt_slot: noimpl_lt,
contains_slot: None,
arith_slot: noimpl_arith,
iter_slot: None,
get_item_slot: None,
set_item_slot: None,
del_item_slot: None,
missing_slot: None,
len_slot: None,
get_attr_slot: None,
set_attr_slot: None,
has_methods_table: false,
};
static DECIMAL_TYPE: TypeObject = TypeObject {
name: "Decimal",
eq_slot: decimal_eq,
hash_slot: Some(decimal_hash_slot),
lt_slot: decimal_lt,
contains_slot: None,
arith_slot: decimal_arith,
iter_slot: None,
get_item_slot: None,
set_item_slot: None,
del_item_slot: None,
missing_slot: None,
len_slot: None,
get_attr_slot: None,
set_attr_slot: None,
has_methods_table: false,
};
static FRACTION_TYPE: TypeObject = TypeObject {
name: "Fraction",
eq_slot: fraction_eq,
hash_slot: Some(fraction_hash_slot),
lt_slot: fraction_lt,
contains_slot: None,
arith_slot: fraction_arith,
iter_slot: None,
get_item_slot: None,
set_item_slot: None,
del_item_slot: None,
missing_slot: None,
len_slot: None,
get_attr_slot: Some(fraction_get_attr),
set_attr_slot: None,
has_methods_table: false,
};
static OBJECT_TYPE: TypeObject = TypeObject {
name: "object",
eq_slot: object_eq,
hash_slot: Some(fallback_hash_slot),
lt_slot: noimpl_lt,
contains_slot: Some(object_contains),
arith_slot: noimpl_arith,
iter_slot: None,
get_item_slot: None,
set_item_slot: None,
del_item_slot: None,
missing_slot: None,
len_slot: None,
get_attr_slot: None,
set_attr_slot: None,
has_methods_table: false,
};
#[expect(
clippy::unnecessary_wraps,
reason = "slot fns return Option<bool> to fit the EqSlot fn-pointer type; None means NotImplemented (try the other operand). Same-type slots always handle, so they always Some(...); breaking the protocol would require a separate slot table per arity."
)]
const fn none_eq(_lhs: &Value, rhs: &Value) -> Option<bool> {
Some(matches!(rhs, Value::None))
}
fn bool_eq(lhs: &Value, rhs: &Value) -> Option<bool> {
let Value::Bool(a) = lhs else { return None };
match rhs {
Value::Bool(b) => Some(a == b),
Value::Int(i) => Some(*i == i64::from(*a)),
Value::BigInt(i) => Some(i.as_ref() == &num_bigint::BigInt::from(i64::from(*a))),
Value::Float(f) => Some(*f == if *a { 1.0 } else { 0.0 }),
_ => None,
}
}
fn int_eq(lhs: &Value, rhs: &Value) -> Option<bool> {
let a = crate::value::value_as_bigint(lhs)?;
match rhs {
Value::Int(_) | Value::BigInt(_) | Value::Bool(_) => {
let b = crate::value::value_as_bigint(rhs)?;
Some(a == b)
}
Value::Float(f) => {
use num_traits::ToPrimitive as _;
Some(a.to_f64().is_some_and(|af| *f == af))
}
_ => None,
}
}
#[expect(
clippy::cast_precision_loss,
reason = "Python int↔float eq matches CPython's lossy compare"
)]
fn float_eq(lhs: &Value, rhs: &Value) -> Option<bool> {
let Value::Float(a) = lhs else { return None };
match rhs {
Value::Float(b) => Some(a == b),
Value::Bool(b) => Some(*a == if *b { 1.0 } else { 0.0 }),
Value::Int(i) => Some(*a == (*i as f64)),
_ => None,
}
}
fn str_eq(lhs: &Value, rhs: &Value) -> Option<bool> {
let Value::String(a) = lhs else { return None };
let Value::String(b) = rhs else { return None };
Some(a == b)
}
#[must_use]
pub fn memoryview_bytes(value: &Value) -> Vec<u8> {
bytes_view(value).unwrap_or_default()
}
fn bytes_view(value: &Value) -> Option<Vec<u8>> {
match value {
Value::Bytes(b) => Some(b.clone()),
Value::ByteArray(b) => Some(b.lock().clone()),
Value::MemoryView(inner) => bytes_view(inner),
_ => None,
}
}
fn bytes_eq(lhs: &Value, rhs: &Value) -> Option<bool> {
Some(bytes_view(lhs)? == bytes_view(rhs)?)
}
fn list_eq(lhs: &Value, rhs: &Value) -> Option<bool> {
let Value::List(a) = lhs else { return None };
let Value::List(b) = rhs else { return None };
if std::sync::Arc::ptr_eq(a, b) {
return Some(true);
}
let a = a.lock().clone();
let b = b.lock().clone();
Some(elementwise_eq(&a, &b))
}
fn tuple_eq(lhs: &Value, rhs: &Value) -> Option<bool> {
let Value::Tuple(a) = lhs else { return None };
let Value::Tuple(b) = rhs else { return None };
Some(elementwise_eq(a, b))
}
fn dict_eq(lhs: &Value, rhs: &Value) -> Option<bool> {
let a = lhs.as_dict()?;
let b = rhs.as_dict()?;
if std::sync::Arc::ptr_eq(a, b) {
return Some(true);
}
let a = a.lock().clone();
let b = b.lock().clone();
if a.len() != b.len() {
return Some(false);
}
let equal = a.iter().all(|(k, v)| b.get(k).is_some_and(|bv| recurse_eq(v, bv)));
Some(equal)
}
fn ordered_dict_eq(lhs: &Value, rhs: &Value) -> Option<bool> {
if let (Value::OrderedDict(a), Value::OrderedDict(b)) = (lhs, rhs) {
if std::sync::Arc::ptr_eq(a, b) {
return Some(true);
}
let a = a.lock().clone();
let b = b.lock().clone();
if a.len() != b.len() {
return Some(false);
}
let equal =
a.iter().zip(b.iter()).all(|((ka, va), (kb, vb))| ka == kb && recurse_eq(va, vb));
return Some(equal);
}
dict_eq(lhs, rhs)
}
fn set_eq(lhs: &Value, rhs: &Value) -> Option<bool> {
let a = lhs.set_items()?;
let b = rhs.set_items()?;
if a.len() != b.len() {
return Some(false);
}
let equal = a.iter().all(|av| b.iter().any(|bv| recurse_eq(av, bv)));
Some(equal)
}
fn set_lt(lhs: &Value, rhs: &Value) -> Option<Result<bool, EvalError>> {
let a = lhs.set_items()?;
let b = rhs.set_items()?;
let is_proper = a.len() < b.len() && a.iter().all(|av| b.iter().any(|bv| recurse_eq(av, bv)));
Some(Ok(is_proper))
}
#[expect(
clippy::unnecessary_wraps,
reason = "EqSlot fn-pointer protocol requires Option<bool>; object_eq always handles via the shared comparator so Some(...) is correct"
)]
fn object_eq(lhs: &Value, rhs: &Value) -> Option<bool> {
Some(crate::eval::operations::values_equal_pub(lhs, rhs))
}
fn elementwise_eq(a: &[Value], b: &[Value]) -> bool {
a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| recurse_eq(x, y))
}
pub(crate) fn recurse_eq(lhs: &Value, rhs: &Value) -> bool {
let Some(_depth) = crate::cycle::eq_depth_enter() else {
return false;
};
let lhs_type = type_of(lhs);
if let Some(result) = (lhs_type.eq_slot)(lhs, rhs) {
return result;
}
let rhs_type = type_of(rhs);
if let Some(result) = (rhs_type.eq_slot)(rhs, lhs) {
return result;
}
false
}
const fn noimpl_lt(_lhs: &Value, _rhs: &Value) -> Option<Result<bool, EvalError>> {
None
}
fn date_lt(lhs: &Value, rhs: &Value) -> Option<Result<bool, EvalError>> {
match (lhs, rhs) {
(Value::Date(a), Value::Date(b)) => Some(Ok(a < b)),
_ => None,
}
}
fn time_lt(lhs: &Value, rhs: &Value) -> Option<Result<bool, EvalError>> {
match (lhs, rhs) {
(Value::Time(a), Value::Time(b)) => Some(Ok(a < b)),
_ => None,
}
}
fn timedelta_lt(lhs: &Value, rhs: &Value) -> Option<Result<bool, EvalError>> {
match (lhs, rhs) {
(Value::TimeDelta(a), Value::TimeDelta(b)) => Some(Ok(a < b)),
_ => None,
}
}
fn datetime_lt(lhs: &Value, rhs: &Value) -> Option<Result<bool, EvalError>> {
let (
Value::DateTime { dt: a, tz_offset_secs: ta },
Value::DateTime { dt: b, tz_offset_secs: tb },
) = (lhs, rhs)
else {
return None;
};
match (ta, tb) {
(None, None) => Some(Ok(a < b)),
(Some(oa), Some(ob)) => {
let ia = *a - chrono::Duration::seconds(i64::from(*oa));
let ib = *b - chrono::Duration::seconds(i64::from(*ob));
Some(Ok(ia < ib))
}
_ => Some(Err(InterpreterError::TypeError(
"can't compare offset-naive and offset-aware datetimes".into(),
)
.into())),
}
}
fn bool_lt(lhs: &Value, rhs: &Value) -> Option<Result<bool, EvalError>> {
let Value::Bool(a) = lhs else { return None };
let av = i64::from(*a);
match rhs {
Value::Bool(b) => Some(Ok(av < i64::from(*b))),
Value::Int(b) => Some(Ok(av < *b)),
#[expect(
clippy::cast_precision_loss,
reason = "Python bool↔float compare matches CPython's lossy compare"
)]
Value::Float(b) => Some(Ok((av as f64) < *b)),
_ => None,
}
}
fn int_lt(lhs: &Value, rhs: &Value) -> Option<Result<bool, EvalError>> {
let a = crate::value::value_as_bigint(lhs)?;
match rhs {
Value::Int(_) | Value::BigInt(_) | Value::Bool(_) => {
let b = crate::value::value_as_bigint(rhs)?;
Some(Ok(a < b))
}
Value::Float(b) => {
use num_traits::ToPrimitive as _;
Some(Ok(a.to_f64().is_some_and(|af| af < *b)))
}
_ => None,
}
}
fn float_lt(lhs: &Value, rhs: &Value) -> Option<Result<bool, EvalError>> {
let Value::Float(a) = lhs else { return None };
match rhs {
Value::Float(b) => Some(Ok(a < b)),
Value::Bool(b) => Some(Ok(*a < if *b { 1.0 } else { 0.0 })),
#[expect(
clippy::cast_precision_loss,
reason = "Python int↔float compare matches CPython's lossy compare"
)]
Value::Int(b) => Some(Ok(*a < (*b as f64))),
Value::BigInt(b) => {
use num_traits::ToPrimitive as _;
Some(Ok(b.to_f64().is_some_and(|bf| *a < bf)))
}
_ => None,
}
}
fn str_lt(lhs: &Value, rhs: &Value) -> Option<Result<bool, EvalError>> {
let Value::String(a) = lhs else { return None };
let Value::String(b) = rhs else { return None };
Some(Ok(a < b))
}
pub(crate) fn value_to_complex(v: &Value) -> Option<num_complex::Complex64> {
use num_traits::ToPrimitive as _;
let re = match v {
Value::Complex(c) => return Some(**c),
Value::Float(f) => *f,
#[expect(clippy::cast_precision_loss, reason = "matches Python complex(int) coercion")]
Value::Int(i) => *i as f64,
Value::Bool(b) => f64::from(*b),
Value::BigInt(b) => b.to_f64()?,
_ => return None,
};
Some(num_complex::Complex64::new(re, 0.0))
}
fn complex_arith(
op: BinOp,
lhs: &Value,
rhs: &Value,
_decimal_prec: i64,
) -> Option<Result<Value, EvalError>> {
if !matches!(lhs, Value::Complex(_)) && !matches!(rhs, Value::Complex(_)) {
return None;
}
let a = value_to_complex(lhs)?;
let b = value_to_complex(rhs)?;
let out = match op {
BinOp::Add => a + b,
BinOp::Sub => a - b,
BinOp::Mul => a * b,
BinOp::Div => {
if b.re == 0.0 && b.im == 0.0 {
return Some(Err(EvalError::Exception(crate::value::ExceptionValue::new(
"ZeroDivisionError",
"complex division by zero",
))));
}
a / b
}
BinOp::Pow => match rhs {
Value::Int(n) => i32::try_from(*n).map_or_else(|_| a.powc(b), |e| a.powi(e)),
Value::Bool(bl) => a.powi(i32::from(*bl)),
_ => a.powc(b),
},
BinOp::FloorDiv | BinOp::Mod => {
return Some(Err(InterpreterError::TypeError(
"can't take floor or mod of complex number.".into(),
)
.into()));
}
};
Some(Ok(Value::Complex(Box::new(out))))
}
fn complex_eq(lhs: &Value, rhs: &Value) -> Option<bool> {
if !matches!(lhs, Value::Complex(_)) && !matches!(rhs, Value::Complex(_)) {
return None;
}
match (value_to_complex(lhs), value_to_complex(rhs)) {
(Some(a), Some(b)) => Some(a == b),
_ => Some(false),
}
}
fn complex_hash_slot(value: &Value) -> Result<i64, EvalError> {
let Value::Complex(c) = value else { unreachable!("complex_hash_slot sees only Complex") };
let combined =
float_hash_impl(c.re).wrapping_add(1_000_003_i64.wrapping_mul(float_hash_impl(c.im)));
Ok(finalize_hash(combined))
}
fn bytes_lt(lhs: &Value, rhs: &Value) -> Option<Result<bool, EvalError>> {
let (a, b) = (bytes_view(lhs)?, bytes_view(rhs)?);
Some(Ok(a < b))
}
fn list_lt(lhs: &Value, rhs: &Value) -> Option<Result<bool, EvalError>> {
let Value::List(a) = lhs else { return None };
let Value::List(b) = rhs else { return None };
if std::sync::Arc::ptr_eq(a, b) {
return Some(Ok(false));
}
let a_guard = a.lock();
let b_guard = b.lock();
Some(lex_lt(&a_guard, &b_guard))
}
fn tuple_lt(lhs: &Value, rhs: &Value) -> Option<Result<bool, EvalError>> {
let Value::Tuple(a) = lhs else { return None };
let Value::Tuple(b) = rhs else { return None };
Some(lex_lt(a, b))
}
fn lex_lt(a: &[Value], b: &[Value]) -> Result<bool, EvalError> {
for (x, y) in a.iter().zip(b.iter()) {
if !recurse_eq(x, y) {
return dispatch_lt(x, y);
}
}
Ok(a.len() < b.len())
}
#[expect(
clippy::unnecessary_wraps,
reason = "ContainsSlot protocol fixes the Result<bool, EvalError> signature; slots that can't error still keep it so call sites stay homogeneous across all container types"
)]
fn sequence_contains(container: &Value, item: &Value) -> Result<bool, EvalError> {
if let Value::List(items) = container {
let snapshot = items.lock().clone();
for entry in &snapshot {
if recurse_eq(item, entry) {
return Ok(true);
}
}
return Ok(false);
}
match container {
Value::Set(b) => return Ok(b.lock().contains(item)),
Value::Frozenset(b) => return Ok(b.contains(item)),
_ => {}
}
let Value::Tuple(items) = container else {
unreachable!("sequence_contains only attached to list/tuple/set TypeObjects")
};
for entry in items {
if recurse_eq(item, entry) {
return Ok(true);
}
}
Ok(false)
}
fn dict_contains(container: &Value, item: &Value) -> Result<bool, EvalError> {
let Some(map) = container.as_dict() else {
unreachable!("dict_contains only on dict/OrderedDict types")
};
let key = crate::eval::literals::value_to_key(item)?;
Ok(map.lock().contains_key(&key))
}
fn str_contains(container: &Value, item: &Value) -> Result<bool, EvalError> {
let Value::String(s) = container else { unreachable!("str_contains only on STR_TYPE") };
let Value::String(needle) = item else {
return Err(InterpreterError::TypeError(format!(
"'in <string>' requires string as left operand, not '{}'",
item.type_name()
))
.into());
};
Ok(s.contains(needle.as_str()))
}
fn bytes_contains(container: &Value, item: &Value) -> Result<bool, EvalError> {
let haystack = bytes_view(container).unwrap_or_default();
match item {
Value::Int(_) | Value::Bool(_) => {
let n = match item {
Value::Int(i) => *i,
Value::Bool(b) => i64::from(*b),
_ => unreachable!(),
};
if !(0..=255).contains(&n) {
return Err(
InterpreterError::ValueError("byte must be in range(0, 256)".into()).into()
);
}
Ok(haystack.iter().any(|&b| i64::from(b) == n))
}
Value::Bytes(_) | Value::ByteArray(_) | Value::MemoryView(_) => {
let needle = bytes_view(item).unwrap_or_default();
Ok(needle.is_empty()
|| haystack.windows(needle.len()).any(|window| window == needle.as_slice()))
}
other => Err(InterpreterError::TypeError(format!(
"a bytes-like object is required, not '{}'",
other.type_name()
))
.into()),
}
}
fn object_contains(container: &Value, _item: &Value) -> Result<bool, EvalError> {
Err(InterpreterError::TypeError(format!(
"argument of type '{}' is not iterable",
container.type_name(),
))
.into())
}
const fn noimpl_arith(
_op: BinOp,
_lhs: &Value,
_rhs: &Value,
_decimal_prec: i64,
) -> Option<Result<Value, EvalError>> {
None
}
fn numeric_arith(
op: BinOp,
lhs: &Value,
rhs: &Value,
_decimal_prec: i64,
) -> Option<Result<Value, EvalError>> {
if !is_numeric(lhs) {
return None;
}
if is_numeric(rhs) {
return Some(crate::eval::operations::apply_binop_builtin(op, lhs, rhs));
}
if matches!(op, BinOp::Mul)
&& matches!(rhs, Value::String(_) | Value::List(_) | Value::Tuple(_))
{
return Some(crate::eval::operations::apply_binop_builtin(op, lhs, rhs));
}
None
}
fn str_arith(
op: BinOp,
lhs: &Value,
rhs: &Value,
_decimal_prec: i64,
) -> Option<Result<Value, EvalError>> {
let Value::String(_) = lhs else { return None };
match op {
BinOp::Add if matches!(rhs, Value::String(_)) => {
Some(crate::eval::operations::apply_binop_builtin(op, lhs, rhs))
}
BinOp::Mul if matches!(rhs, Value::Int(_) | Value::Bool(_)) => {
Some(crate::eval::operations::apply_binop_builtin(op, lhs, rhs))
}
BinOp::Mod => Some(crate::eval::operations::apply_binop_builtin(op, lhs, rhs)),
_ => None,
}
}
fn bytes_arith(
op: BinOp,
lhs: &Value,
rhs: &Value,
_decimal_prec: i64,
) -> Option<Result<Value, EvalError>> {
let (Value::Bytes(_) | Value::ByteArray(_)) = lhs else { return None };
match op {
BinOp::Add if matches!(rhs, Value::Bytes(_) | Value::ByteArray(_)) => {
Some(crate::eval::operations::apply_binop_builtin(op, lhs, rhs))
}
BinOp::Mul if matches!(rhs, Value::Int(_) | Value::Bool(_)) => {
Some(crate::eval::operations::apply_binop_builtin(op, lhs, rhs))
}
BinOp::Mod => Some(crate::eval::operations::apply_binop_builtin(op, lhs, rhs)),
_ => None,
}
}
fn list_arith(
op: BinOp,
lhs: &Value,
rhs: &Value,
_decimal_prec: i64,
) -> Option<Result<Value, EvalError>> {
let Value::List(_) = lhs else { return None };
match op {
BinOp::Add if matches!(rhs, Value::List(_)) => {
Some(crate::eval::operations::apply_binop_builtin(op, lhs, rhs))
}
BinOp::Mul if matches!(rhs, Value::Int(_) | Value::Bool(_)) => {
Some(crate::eval::operations::apply_binop_builtin(op, lhs, rhs))
}
_ => None,
}
}
fn array_arith(op: BinOp, lhs: &Value, rhs: &Value) -> Option<Result<Value, EvalError>> {
let Value::Array { typecode, items } = lhs else { return None };
match op {
BinOp::Add => {
let Value::Array { typecode: rt, items: ri } = rhs else {
return Some(Err(InterpreterError::TypeError(format!(
"can only append array (not \"{}\") to array",
rhs.python_type_name()
))
.into()));
};
if rt != typecode {
return Some(Err(InterpreterError::TypeError(
"bad argument type for built-in operation".into(),
)
.into()));
}
let mut combined = items.lock().clone();
combined.extend(ri.lock().iter().cloned());
Some(Ok(Value::Array {
typecode: *typecode,
items: crate::value::shared_list(combined),
}))
}
BinOp::Mul => {
let n = match rhs {
Value::Int(i) => *i,
Value::Bool(b) => i64::from(*b),
_ => return None,
};
let src = items.lock().clone();
let mut out = Vec::new();
for _ in 0..n.max(0) {
out.extend(src.iter().cloned());
}
Some(Ok(Value::Array { typecode: *typecode, items: crate::value::shared_list(out) }))
}
_ => None,
}
}
fn tuple_arith(
op: BinOp,
lhs: &Value,
rhs: &Value,
_decimal_prec: i64,
) -> Option<Result<Value, EvalError>> {
let Value::Tuple(_) = lhs else { return None };
match op {
BinOp::Add if matches!(rhs, Value::Tuple(_)) => {
Some(crate::eval::operations::apply_binop_builtin(op, lhs, rhs))
}
BinOp::Mul if matches!(rhs, Value::Int(_) | Value::Bool(_)) => {
Some(crate::eval::operations::apply_binop_builtin(op, lhs, rhs))
}
_ => None,
}
}
fn set_arith(
op: BinOp,
lhs: &Value,
rhs: &Value,
_decimal_prec: i64,
) -> Option<Result<Value, EvalError>> {
let (Value::Set(_) | Value::Frozenset(_)) = lhs else { return None };
match op {
BinOp::Sub if matches!(rhs, Value::Set(_) | Value::Frozenset(_)) => {
Some(crate::eval::operations::apply_binop_builtin(op, lhs, rhs))
}
_ => None,
}
}
const fn is_numeric(v: &Value) -> bool {
matches!(v, Value::Int(_) | Value::BigInt(_) | Value::Float(_) | Value::Bool(_))
}
#[expect(
clippy::unnecessary_wraps,
reason = "IterSlot protocol fixes the Result<Vec<Value>, EvalError> signature; same-type iter slots always succeed but keep the protocol so call sites stay homogeneous"
)]
fn sequence_iter(value: &Value) -> Result<Vec<Value>, EvalError> {
if let Value::List(items) = value {
return Ok(items.lock().clone());
}
let Value::Tuple(items) = value else {
unreachable!("sequence_iter only attached to list/tuple TypeObjects")
};
Ok(items.clone())
}
fn set_iter(value: &Value) -> Result<Vec<Value>, EvalError> {
match value {
Value::Set(b) => Ok(b.lock().iter_ordered()),
Value::Frozenset(b) => Ok(b.iter_ordered()),
_ => unreachable!("set_iter only attached to set/frozenset TypeObjects"),
}
}
#[expect(
clippy::unnecessary_wraps,
reason = "IterSlot protocol; str iteration cannot fail at the materialization step"
)]
fn str_iter(value: &Value) -> Result<Vec<Value>, EvalError> {
let Value::String(s) = value else { unreachable!("str_iter only on STR_TYPE") };
Ok(s.chars().map(|c| Value::String(c.to_string().into())).collect())
}
#[expect(clippy::unnecessary_wraps, reason = "IterSlot protocol; bytes iteration cannot fail")]
fn bytes_iter(value: &Value) -> Result<Vec<Value>, EvalError> {
let b = bytes_view(value).unwrap_or_default();
Ok(b.iter().map(|&byte| Value::Int(i64::from(byte))).collect())
}
#[expect(clippy::unnecessary_wraps, reason = "IterSlot protocol; dict iteration cannot fail")]
fn dict_iter(value: &Value) -> Result<Vec<Value>, EvalError> {
let Some(map) = value.as_dict() else {
unreachable!("dict_iter only on dict/OrderedDict types")
};
Ok(map.lock().keys().map(crate::value::ValueKey::to_value).collect())
}
fn range_iter(value: &Value) -> Result<Vec<Value>, EvalError> {
let Value::Range { start, stop, step } = value else {
unreachable!("range_iter only on RANGE_TYPE")
};
let (s, e, st) = (i128::from(*start), i128::from(*stop), i128::from(*step));
let span = e - s;
let count: i128 = if (st > 0 && span > 0) || (st < 0 && span < 0) {
span / st + i128::from(span % st != 0)
} else {
0
};
if count > crate::eval::operations::MAX_COLLECTION_SIZE as i128 {
return Err(InterpreterError::LimitExceeded(format!(
"range with {count} elements is too large to materialise (limit: {})",
crate::eval::operations::MAX_COLLECTION_SIZE
))
.into());
}
let mut items = Vec::new();
let mut i = *start;
match (*step).cmp(&0) {
std::cmp::Ordering::Greater => {
while i < *stop {
items.push(Value::Int(i));
i += step;
}
}
std::cmp::Ordering::Less => {
while i > *stop {
items.push(Value::Int(i));
i += step;
}
}
std::cmp::Ordering::Equal => {}
}
Ok(items)
}
#[expect(
clippy::unnecessary_wraps,
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::float_cmp,
reason = "ContainsSlot protocol; the round-trip-guarded float→int fold matches CPython's bool/float/int numeric equivalence"
)]
fn range_contains(container: &Value, item: &Value) -> Result<bool, EvalError> {
let Value::Range { start, stop, step } = container else {
unreachable!("range_contains only on RANGE_TYPE")
};
let val: i64 = match item {
Value::Int(n) => *n,
Value::Bool(b) => i64::from(*b),
Value::Float(f) => {
if !f.is_finite() || f.fract() != 0.0 {
return Ok(false);
}
let as_int = *f as i64;
if as_int as f64 != *f {
return Ok(false);
}
as_int
}
_ => return Ok(false),
};
if *step == 0 {
return Ok(false);
}
let in_bounds =
if *step > 0 { val >= *start && val < *stop } else { val <= *start && val > *stop };
Ok(in_bounds && (val - *start) % *step == 0)
}
const HASH_BITS: u32 = 61;
const HASH_MODULUS: u64 = (1u64 << HASH_BITS) - 1;
const HASH_INF: i64 = 314_159;
const fn finalize_hash(h: i64) -> i64 {
if h == -1 { -2 } else { h }
}
fn none_hash(_value: &Value) -> Result<i64, EvalError> {
Ok(0)
}
fn bool_hash(value: &Value) -> Result<i64, EvalError> {
let Value::Bool(b) = value else { unreachable!("bool_hash sees only Value::Bool") };
Ok(finalize_hash(int_hash_impl(i64::from(*b))))
}
fn int_hash_slot(value: &Value) -> Result<i64, EvalError> {
Ok(match value {
Value::Int(n) => finalize_hash(int_hash_impl(*n)),
Value::BigInt(n) => {
use num_traits::{Signed, ToPrimitive as _};
let modulus = num_bigint::BigInt::from(HASH_MODULUS);
let mut rem = n.abs() % &modulus;
if n.sign() == num_bigint::Sign::Minus {
rem = -rem;
}
finalize_hash(rem.to_i64().unwrap_or(0))
}
_ => unreachable!("int_hash_slot sees only int variants"),
})
}
#[expect(
clippy::cast_possible_wrap,
reason = "abs is bounded by HASH_MODULUS (~2^61), well within i64::MAX; the cast is sign-preserving"
)]
const fn int_hash_impl(n: i64) -> i64 {
let abs = n.unsigned_abs() % HASH_MODULUS;
if n < 0 { -(abs as i64) } else { abs as i64 }
}
fn float_hash_slot(value: &Value) -> Result<i64, EvalError> {
let Value::Float(f) = value else { unreachable!("float_hash_slot sees only Value::Float") };
Ok(finalize_hash(float_hash_impl(*f)))
}
fn mulmod(a: u64, b: u64, m: u64) -> u64 {
((u128::from(a) * u128::from(b)) % u128::from(m)) as u64
}
fn powmod(base: u64, mut exp: u64, m: u64) -> u64 {
let mut base = base % m;
let mut result = 1u64;
while exp > 0 {
if exp & 1 == 1 {
result = mulmod(result, base, m);
}
base = mulmod(base, base, m);
exp >>= 1;
}
result
}
fn rational_hash(n_abs_mod: u64, d_mod: u64, negative: bool) -> i64 {
let hash_abs = if d_mod == 0 {
HASH_INF
} else {
let d_inv = powmod(d_mod, HASH_MODULUS - 2, HASH_MODULUS);
mulmod(n_abs_mod, d_inv, HASH_MODULUS) as i64
};
finalize_hash(if negative { -hash_abs } else { hash_abs })
}
fn bigint_abs_mod(n: &num_bigint::BigInt) -> u64 {
use num_traits::{Signed as _, ToPrimitive as _};
(n.abs() % num_bigint::BigInt::from(HASH_MODULUS)).to_u64().unwrap_or(0)
}
fn decimal_hash_slot(value: &Value) -> Result<i64, EvalError> {
use num_traits::Signed as _;
let Value::Decimal(d, _) = value else { unreachable!("decimal_hash_slot sees only Decimal") };
let (mantissa, scale) = d.as_bigint_and_exponent();
let m_mod = bigint_abs_mod(&mantissa);
let (n_abs_mod, d_mod) = if scale >= 0 {
(m_mod, powmod(10, u64::try_from(scale).unwrap_or(0), HASH_MODULUS))
} else {
(
mulmod(
m_mod,
powmod(10, u64::try_from(-scale).unwrap_or(0), HASH_MODULUS),
HASH_MODULUS,
),
1,
)
};
Ok(rational_hash(n_abs_mod, d_mod, mantissa.is_negative()))
}
fn fraction_hash_slot(value: &Value) -> Result<i64, EvalError> {
use num_traits::Signed as _;
let Value::Fraction(fr) = value else { unreachable!("fraction_hash_slot sees only Fraction") };
let n_abs_mod = bigint_abs_mod(fr.numer());
let d_mod = bigint_abs_mod(fr.denom());
Ok(rational_hash(n_abs_mod, d_mod, fr.numer().is_negative()))
}
#[must_use]
pub(crate) fn rational_number_hash(value: &Value) -> Option<i64> {
match value {
Value::Decimal(..) => decimal_hash_slot(value).ok(),
Value::Fraction(_) => fraction_hash_slot(value).ok(),
_ => None,
}
}
#[expect(
clippy::cast_possible_wrap,
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::cast_sign_loss,
reason = "translation of CPython's _Py_HashDouble — every cast mirrors the C version's semantics and operates on bounded values"
)]
#[expect(
clippy::many_single_char_names,
reason = "matches CPython's _Py_HashDouble variable names verbatim (m mantissa, e exponent, x accumulator, y integer-part-of-shifted-mantissa, v input) for line-by-line traceability against Python/pyhash.c"
)]
#[expect(
clippy::while_float,
reason = "termination follows CPython's invariant that the 28-bit-per-iteration shift drains the mantissa to exact 0.0 within ceil(53/28) iterations on a finite f64"
)]
fn float_hash_impl(v: f64) -> i64 {
if !v.is_finite() {
if v.is_infinite() {
return if v > 0.0 { HASH_INF } else { -HASH_INF };
}
return 0;
}
let sign: i64 = if v < 0.0 { -1 } else { 1 };
let (mut m, mut e) = frexp(v.abs());
let mut x: u64 = 0;
while m != 0.0 {
x = ((x << 28) & HASH_MODULUS) | (x >> (HASH_BITS - 28));
m *= 268_435_456.0; e -= 28;
let y = m as u64;
m -= y as f64;
x = x.wrapping_add(y);
if x >= HASH_MODULUS {
x -= HASH_MODULUS;
}
}
let e_adj: u32 = if e >= 0 {
(e as u32) % HASH_BITS
} else {
HASH_BITS - 1 - (((-1 - e) as u32) % HASH_BITS)
};
x = ((x << e_adj) & HASH_MODULUS) | (x >> (HASH_BITS - e_adj));
(x as i64).wrapping_mul(sign)
}
fn frexp(v: f64) -> (f64, i32) {
if v == 0.0 || !v.is_finite() {
return (v, 0);
}
let bits = v.to_bits();
let biased_exp = ((bits >> 52) & 0x7FF) as i32;
if biased_exp == 0 {
let scaled = v * f64::from_bits((1023u64 + 54) << 52); let (m, e) = frexp(scaled);
return (m, e - 54);
}
let new_bits = (bits & !(0x7FFu64 << 52)) | (1022u64 << 52);
let m = f64::from_bits(new_bits);
let e = biased_exp - 1022;
(m, e)
}
#[expect(
clippy::cast_possible_wrap,
reason = "Python's hash() returns a signed integer; reinterpreting u64 bits as i64 via wrapping matches CPython's Py_hash_t on 64-bit platforms"
)]
fn fallback_hash_slot(value: &Value) -> Result<i64, EvalError> {
use std::hash::{Hash as _, Hasher as _};
if let Some(h) = crate::pyhash::python_hash(value) {
return Ok(h);
}
let key = crate::eval::literals::value_to_key(value)?;
let mut hasher = std::collections::hash_map::DefaultHasher::new();
key.hash(&mut hasher);
Ok(finalize_hash(hasher.finish() as i64))
}
fn sequence_get_item(container: &Value, index: &Value) -> Result<Value, EvalError> {
if let Value::List(items) = container {
let guard = items.lock();
let raw = int_index(index, "list")?;
let idx = normalize_seq_index(raw, guard.len(), "list")?;
return Ok(guard[idx].clone());
}
let Value::Tuple(items) = container else {
unreachable!("sequence_get_item only on list/tuple TypeObjects")
};
let raw = int_index(index, "tuple")?;
let idx = normalize_seq_index(raw, items.len(), "tuple")?;
Ok(items[idx].clone())
}
fn str_get_item(container: &Value, index: &Value) -> Result<Value, EvalError> {
let Value::String(s) = container else { unreachable!("str_get_item only on STR_TYPE") };
let raw = int_index(index, "string")?;
let chars: Vec<char> = s.chars().collect();
let idx = normalize_seq_index(raw, chars.len(), "string")?;
Ok(Value::String(chars[idx].to_string().into()))
}
fn bytes_get_item(container: &Value, index: &Value) -> Result<Value, EvalError> {
let b = bytes_view(container).unwrap_or_default();
let name = if matches!(container, Value::ByteArray(_)) { "bytearray" } else { "bytes" };
let raw = int_index(index, name)?;
let idx = normalize_seq_index(raw, b.len(), "bytes")?;
Ok(Value::Int(i64::from(b[idx])))
}
fn dict_get_item(container: &Value, index: &Value) -> Result<Value, EvalError> {
let Some(map) = container.as_dict() else {
unreachable!("dict_get_item only on dict/OrderedDict types")
};
let key = crate::eval::literals::value_to_key(index)?;
if let Some(value) = map.lock().get(&key).cloned() {
return Ok(value);
}
if let Some(missing) = type_of(container).missing_slot {
return missing(container, index);
}
Err(crate::value::ExceptionValue::key_error(&key).into())
}
fn range_get_item(container: &Value, index: &Value) -> Result<Value, EvalError> {
let Value::Range { start, stop, step } = container else {
unreachable!("range_get_item only on RANGE_TYPE")
};
let raw = int_index(index, "range")?;
let len = range_length(*start, *stop, *step);
let idx = normalize_seq_index(raw, len, "range object")?;
let idx_i64 = i64::try_from(idx)
.map_err(|_| EvalError::from(InterpreterError::Runtime("range index overflow".into())))?;
Ok(Value::Int(start + idx_i64 * step))
}
fn list_set_item(container: &mut Value, index: &Value, value: Value) -> Result<isize, EvalError> {
let Value::List(items) = container else { unreachable!("list_set_item only on LIST_TYPE") };
let raw = int_index(index, "list")?;
let mut guard = items.lock();
let idx = normalize_seq_index(raw, guard.len(), "list")?;
let delta = size_delta(
crate::state::estimate_value_size(&guard[idx]),
crate::state::estimate_value_size(&value),
);
guard[idx] = value;
drop(guard);
Ok(delta)
}
fn dict_set_item(container: &mut Value, index: &Value, value: Value) -> Result<isize, EvalError> {
let Some(map) = container.as_dict() else {
unreachable!("dict_set_item only on dict/OrderedDict types")
};
let key = crate::eval::literals::value_to_key(index)?;
let new_size = crate::state::estimate_value_size(&value);
let delta = map.lock().insert(key.clone(), value).map_or_else(
|| to_isize_sat(crate::state::estimate_key_size(&key) + new_size),
|old| size_delta(crate::state::estimate_value_size(&old), new_size),
);
Ok(delta)
}
fn list_del_item(container: &mut Value, index: &Value) -> Result<isize, EvalError> {
let Value::List(items) = container else { unreachable!("list_del_item only on LIST_TYPE") };
if let Value::Slice(s) = index {
let step = match &s.step {
Value::None => 1,
Value::Int(n) => *n,
Value::Bool(b) => i64::from(*b),
_ => {
return Err(InterpreterError::TypeError(
"slice indices must be integers or None or have an __index__ method"
.to_string(),
)
.into());
}
};
if step == 0 {
return Err(InterpreterError::ValueError("slice step cannot be zero".into()).into());
}
let mut guard = items.lock();
let len = i64::try_from(guard.len()).unwrap_or(i64::MAX);
let indices =
crate::eval::delete::strided_indices(Some(&s.start), Some(&s.stop), step, len);
let mut freed = 0usize;
for &u in &indices {
if u < guard.len() {
freed += crate::state::estimate_value_size(&guard[u]);
guard.remove(u);
}
}
drop(guard);
return Ok(-to_isize_sat(freed));
}
let raw = int_index(index, "list")?;
let mut guard = items.lock();
let idx = normalize_seq_index(raw, guard.len(), "list")?;
let removed = guard.remove(idx);
drop(guard);
Ok(-to_isize_sat(crate::state::estimate_value_size(&removed)))
}
fn dict_del_item(container: &mut Value, index: &Value) -> Result<isize, EvalError> {
let Some(map) = container.as_dict() else {
unreachable!("dict_del_item only on dict/OrderedDict types")
};
let key = crate::eval::literals::value_to_key(index)?;
let Some(val) = map.lock().shift_remove(&key) else {
return Err(crate::value::ExceptionValue::key_error(&key).into());
};
let freed = crate::state::estimate_key_size(&key) + crate::state::estimate_value_size(&val);
Ok(-to_isize_sat(freed))
}
fn int_index(index: &Value, container_name: &str) -> Result<i64, EvalError> {
match index {
Value::Int(i) => Ok(*i),
Value::Bool(b) => Ok(i64::from(*b)),
Value::EnumMember {
value,
kind: crate::value::EnumKind::Int | crate::value::EnumKind::IntFlag,
..
} => int_index(value, container_name),
other => {
let ty = other.type_name();
let msg = match container_name {
"string" => format!("string indices must be integers, not '{ty}'"),
"bytes" => format!("byte indices must be integers or slices, not {ty}"),
_ => format!("{container_name} indices must be integers or slices, not {ty}"),
};
Err(InterpreterError::TypeError(msg).into())
}
}
}
fn normalize_seq_index(raw: i64, len: usize, kind: &str) -> Result<usize, EvalError> {
let len_i = i64::try_from(len).map_err(|_| {
EvalError::from(InterpreterError::Runtime(
"sequence length overflows i64 for indexing".into(),
))
})?;
let adjusted = if raw < 0 { len_i + raw } else { raw };
if adjusted < 0 || adjusted >= len_i {
return Err(crate::value::ExceptionValue::index_error(kind).into());
}
usize::try_from(adjusted).map_err(|_| {
EvalError::from(InterpreterError::Runtime("index overflow (internal invariant)".into()))
})
}
const fn size_delta(old: usize, new: usize) -> isize {
to_isize_sat(new).saturating_sub(to_isize_sat(old))
}
#[expect(
clippy::cast_possible_wrap,
reason = "guarded by the if-check above: n <= isize::MAX before the cast, so the resulting i64 sign bit is always 0"
)]
const fn to_isize_sat(n: usize) -> isize {
if n > isize::MAX as usize { isize::MAX } else { n as isize }
}
#[expect(clippy::unnecessary_wraps, reason = "LenSlot protocol fixes the Result signature")]
fn sequence_len(value: &Value) -> Result<usize, EvalError> {
if let Value::List(items) = value {
return Ok(items.lock().len());
}
if let Some(n) = value.set_len() {
return Ok(n);
}
let Value::Tuple(items) = value else {
unreachable!("sequence_len only on list/tuple/set TypeObjects")
};
Ok(items.len())
}
#[expect(clippy::unnecessary_wraps, reason = "LenSlot protocol")]
fn str_len(value: &Value) -> Result<usize, EvalError> {
let Value::String(s) = value else { unreachable!("str_len only on STR_TYPE") };
Ok(s.chars().count())
}
fn byte_from_value(value: &Value) -> Result<u8, EvalError> {
match value {
Value::Int(n) if (0..=255).contains(n) => Ok(*n as u8),
Value::Bool(b) => Ok(u8::from(*b)),
Value::Int(_) => {
Err(InterpreterError::ValueError("byte must be in range(0, 256)".into()).into())
}
other => Err(InterpreterError::TypeError(format!(
"'{}' object cannot be interpreted as an integer",
other.type_name()
))
.into()),
}
}
fn bytearray_set_item(
container: &mut Value,
index: &Value,
value: Value,
) -> Result<isize, EvalError> {
let Value::ByteArray(ba) = container else {
unreachable!("bytearray_set_item only on BYTEARRAY_TYPE")
};
let byte = byte_from_value(&value)?;
let mut b = ba.lock();
let raw = int_index(index, "bytearray")?;
let idx = normalize_seq_index(raw, b.len(), "bytearray")?;
b[idx] = byte;
Ok(0)
}
fn bytearray_del_item(container: &mut Value, index: &Value) -> Result<isize, EvalError> {
let Value::ByteArray(ba) = container else {
unreachable!("bytearray_del_item only on BYTEARRAY_TYPE")
};
let mut b = ba.lock();
let raw = int_index(index, "bytearray")?;
let idx = normalize_seq_index(raw, b.len(), "bytearray")?;
b.remove(idx);
Ok(-1)
}
fn bytearray_get_attr(value: &Value, name: &str) -> EvalResult {
if BYTEARRAY_METHODS.contains(&name) {
return Ok(bound_method(value, name));
}
Err(attribute_error("bytearray", name))
}
const BYTES_METHODS: &[&str] = &[
"decode",
"hex",
"startswith",
"endswith",
"split",
"rsplit",
"replace",
"find",
"rfind",
"index",
"rindex",
"count",
"upper",
"lower",
"swapcase",
"capitalize",
"title",
"isdigit",
"isalpha",
"isalnum",
"isspace",
"isupper",
"islower",
"istitle",
"isascii",
"strip",
"lstrip",
"rstrip",
"join",
"removeprefix",
"removesuffix",
"translate",
"partition",
"rpartition",
"center",
"ljust",
"rjust",
"zfill",
"splitlines",
"expandtabs",
"fromhex",
"maketrans",
];
fn bytes_get_attr(value: &Value, name: &str) -> EvalResult {
if BYTES_METHODS.contains(&name) {
return Ok(bound_method(value, name));
}
Err(attribute_error("bytes", name))
}
const MEMORYVIEW_METHODS: &[&str] = &["tobytes", "tolist", "hex"];
const INT_METHODS: &[&str] = &[
"bit_length",
"bit_count",
"to_bytes",
"from_bytes",
"as_integer_ratio",
"conjugate",
"is_integer",
];
const FLOAT_METHODS: &[&str] = &["is_integer", "as_integer_ratio", "hex", "fromhex", "conjugate"];
const COMPLEX_METHODS: &[&str] = &["conjugate"];
const RANGE_METHODS: &[&str] = &["count", "index"];
fn memoryview_get_attr(value: &Value, name: &str) -> EvalResult {
if MEMORYVIEW_METHODS.contains(&name) {
return Ok(bound_method(value, name));
}
let len = bytes_view(value).map_or(0, |b| b.len());
let readonly = matches!(value, Value::MemoryView(inner) if matches!(**inner, Value::Bytes(_)));
match name {
"nbytes" => Ok(Value::Int(i64::try_from(len).unwrap_or(i64::MAX))),
"itemsize" | "ndim" => Ok(Value::Int(1)),
"format" => Ok(Value::String("B".into())),
"shape" => Ok(Value::Tuple(vec![Value::Int(i64::try_from(len).unwrap_or(i64::MAX))])),
"strides" => Ok(Value::Tuple(vec![Value::Int(1)])),
"suboffsets" => Ok(Value::Tuple(Vec::new())),
"readonly" => Ok(Value::Bool(readonly)),
"contiguous" | "c_contiguous" | "f_contiguous" => Ok(Value::Bool(true)),
"obj" => match value {
Value::MemoryView(inner) => Ok((**inner).clone()),
_ => Err(attribute_error("memoryview", name)),
},
_ => Err(attribute_error("memoryview", name)),
}
}
#[expect(clippy::unnecessary_wraps, reason = "LenSlot protocol")]
fn bytes_len(value: &Value) -> Result<usize, EvalError> {
let b = bytes_view(value).unwrap_or_default();
Ok(b.len())
}
#[expect(clippy::unnecessary_wraps, reason = "LenSlot protocol")]
fn dict_len(value: &Value) -> Result<usize, EvalError> {
let Some(map) = value.as_dict() else {
unreachable!("dict_len only on dict/OrderedDict types")
};
let len = map.lock().len();
Ok(len)
}
#[expect(clippy::unnecessary_wraps, reason = "LenSlot protocol; range length cannot fail")]
fn range_len(value: &Value) -> Result<usize, EvalError> {
let Value::Range { start, stop, step } = value else {
unreachable!("range_len only on RANGE_TYPE")
};
Ok(range_length(*start, *stop, *step))
}
pub(crate) fn range_length(start: i64, stop: i64, step: i64) -> usize {
let raw = match step.cmp(&0) {
std::cmp::Ordering::Greater => ((stop - start + step - 1) / step).max(0),
std::cmp::Ordering::Less => ((start - stop - step - 1) / (-step)).max(0),
std::cmp::Ordering::Equal => 0,
};
usize::try_from(raw).unwrap_or(0)
}
const DICT_METHODS: &[&str] = &[
"keys",
"values",
"items",
"get",
"pop",
"popitem",
"update",
"setdefault",
"copy",
"clear",
"fromkeys",
];
const STR_METHODS: &[&str] = &[
"upper",
"lower",
"strip",
"lstrip",
"rstrip",
"split",
"rsplit",
"join",
"replace",
"startswith",
"endswith",
"removeprefix",
"removesuffix",
"casefold",
"encode",
"expandtabs",
"partition",
"rpartition",
"find",
"rfind",
"index",
"count",
"format",
"isdigit",
"isalpha",
"isalnum",
"isspace",
"isupper",
"islower",
"title",
"capitalize",
"swapcase",
"center",
"ljust",
"rjust",
"zfill",
"splitlines",
"isidentifier",
"istitle",
"isprintable",
"isascii",
"isdecimal",
"isnumeric",
"translate",
"format_map",
"maketrans",
"rindex",
];
const LIST_METHODS: &[&str] = &[
"append", "extend", "insert", "pop", "remove", "sort", "reverse", "index", "count", "copy",
"clear",
];
const TUPLE_METHODS: &[&str] = &["count", "index"];
const BYTEARRAY_METHODS: &[&str] = &[
"append",
"extend",
"insert",
"remove",
"pop",
"clear",
"reverse",
"copy",
"decode",
"hex",
"upper",
"lower",
"swapcase",
"capitalize",
"title",
"isdigit",
"isalpha",
"isalnum",
"isspace",
"isupper",
"islower",
"strip",
"lstrip",
"rstrip",
"split",
"replace",
"find",
"rfind",
"index",
"rindex",
"count",
"startswith",
"endswith",
"removeprefix",
"removesuffix",
"join",
"isascii",
"istitle",
"expandtabs",
"rsplit",
"translate",
"partition",
"rpartition",
"center",
"ljust",
"rjust",
"zfill",
"splitlines",
"fromhex",
"maketrans",
];
const SET_METHODS: &[&str] = &[
"add",
"remove",
"discard",
"pop",
"clear",
"copy",
"union",
"intersection",
"difference",
"symmetric_difference",
"issubset",
"issuperset",
"isdisjoint",
"update",
"intersection_update",
"difference_update",
"symmetric_difference_update",
];
fn bound_method(value: &Value, attr_name: &str) -> Value {
Value::BoundMethod {
receiver: crate::value::BoundMethodReceiver::Snapshot(Box::new(value.clone())),
method: attr_name.to_string(),
}
}
fn attribute_error(type_name: &str, attr_name: &str) -> EvalError {
InterpreterError::AttributeError(format!("'{type_name}' object has no attribute '{attr_name}'"))
.into()
}
fn noattr_get_attr(value: &Value, name: &str) -> EvalResult {
Err(attribute_error(value.type_name(), name))
}
fn int_get_attr(value: &Value, name: &str) -> EvalResult {
match name {
"real" | "numerator" => Ok(value.clone()),
"imag" => Ok(Value::Int(0)),
"denominator" => Ok(Value::Int(1)),
_ if INT_METHODS.contains(&name) => Ok(bound_method(value, name)),
_ => Err(attribute_error("int", name)),
}
}
fn bool_get_attr(value: &Value, name: &str) -> EvalResult {
let Value::Bool(b) = value else { unreachable!("bool_get_attr sees only Bool") };
let n = i64::from(*b);
match name {
"real" | "numerator" => Ok(Value::Int(n)),
"imag" => Ok(Value::Int(0)),
"denominator" => Ok(Value::Int(1)),
_ if INT_METHODS.contains(&name) => Ok(bound_method(value, name)),
_ => Err(attribute_error("bool", name)),
}
}
fn float_get_attr(value: &Value, name: &str) -> EvalResult {
match name {
"real" => Ok(value.clone()),
"imag" => Ok(Value::Float(0.0)),
_ if FLOAT_METHODS.contains(&name) => Ok(bound_method(value, name)),
_ => Err(attribute_error("float", name)),
}
}
fn complex_get_attr(value: &Value, name: &str) -> EvalResult {
let Value::Complex(c) = value else { unreachable!("complex_get_attr sees only Complex") };
match name {
"real" => Ok(Value::Float(c.re)),
"imag" => Ok(Value::Float(c.im)),
_ if COMPLEX_METHODS.contains(&name) => Ok(bound_method(value, name)),
_ => Err(attribute_error("complex", name)),
}
}
fn range_get_attr(value: &Value, name: &str) -> EvalResult {
let Value::Range { start, stop, step } = value else {
unreachable!("range_get_attr sees only Range")
};
match name {
"start" => Ok(Value::Int(*start)),
"stop" => Ok(Value::Int(*stop)),
"step" => Ok(Value::Int(*step)),
_ if RANGE_METHODS.contains(&name) => Ok(bound_method(value, name)),
_ => Err(attribute_error("range", name)),
}
}
fn dict_get_attr(value: &Value, name: &str) -> EvalResult {
if DICT_METHODS.contains(&name) {
return Ok(bound_method(value, name));
}
Err(attribute_error("dict", name))
}
fn str_get_attr(value: &Value, name: &str) -> EvalResult {
if STR_METHODS.contains(&name) {
return Ok(bound_method(value, name));
}
Err(attribute_error("str", name))
}
fn list_get_attr(value: &Value, name: &str) -> EvalResult {
if LIST_METHODS.contains(&name) {
return Ok(bound_method(value, name));
}
Err(attribute_error("list", name))
}
fn tuple_get_attr(value: &Value, name: &str) -> EvalResult {
if TUPLE_METHODS.contains(&name) {
return Ok(bound_method(value, name));
}
Err(attribute_error("tuple", name))
}
fn set_get_attr(value: &Value, name: &str) -> EvalResult {
if SET_METHODS.contains(&name) {
return Ok(bound_method(value, name));
}
Err(attribute_error("set", name))
}
const FROZENSET_METHODS: &[&str] = &[
"copy",
"union",
"intersection",
"difference",
"symmetric_difference",
"issubset",
"issuperset",
"isdisjoint",
];
fn frozenset_get_attr(value: &Value, name: &str) -> EvalResult {
if FROZENSET_METHODS.contains(&name) {
return Ok(bound_method(value, name));
}
Err(attribute_error("frozenset", name))
}
fn counter_eq(lhs: &Value, rhs: &Value) -> Option<bool> {
let Value::Counter(a) = lhs else { return None };
let compare = |b: &indexmap::IndexMap<crate::value::ValueKey, Value>| {
a.len() == b.len() && a.iter().all(|(k, v)| b.get(k).is_some_and(|bv| recurse_eq(v, bv)))
};
match rhs {
Value::Counter(b) => Some(compare(b)),
Value::Dict(b) => Some(compare(&b.lock())),
_ => None,
}
}
fn counter_contains(container: &Value, item: &Value) -> Result<bool, EvalError> {
let Value::Counter(map) = container else {
unreachable!("counter_contains only on COUNTER_TYPE")
};
let key = crate::eval::literals::value_to_key(item)?;
Ok(map.contains_key(&key))
}
#[expect(clippy::unnecessary_wraps, reason = "IterSlot protocol")]
fn counter_iter(value: &Value) -> Result<Vec<Value>, EvalError> {
let Value::Counter(map) = value else { unreachable!("counter_iter only on COUNTER_TYPE") };
Ok(map.keys().map(crate::value::ValueKey::to_value).collect())
}
fn counter_get_item(container: &Value, index: &Value) -> Result<Value, EvalError> {
let Value::Counter(map) = container else {
unreachable!("counter_get_item only on COUNTER_TYPE")
};
let key = crate::eval::literals::value_to_key(index)?;
if let Some(value) = map.get(&key) {
return Ok(value.clone());
}
if let Some(missing) = type_of(container).missing_slot {
return missing(container, index);
}
Err(crate::value::ExceptionValue::key_error(&key).into())
}
fn counter_set_item(
container: &mut Value,
index: &Value,
value: Value,
) -> Result<isize, EvalError> {
let Value::Counter(map) = container else {
unreachable!("counter_set_item only on COUNTER_TYPE")
};
let key = crate::eval::literals::value_to_key(index)?;
let new_size = crate::state::estimate_value_size(&value);
let delta = map.insert(key.clone(), value).map_or_else(
|| to_isize_sat(crate::state::estimate_key_size(&key) + new_size),
|old| size_delta(crate::state::estimate_value_size(&old), new_size),
);
Ok(delta)
}
fn counter_del_item(container: &mut Value, index: &Value) -> Result<isize, EvalError> {
let Value::Counter(map) = container else {
unreachable!("counter_del_item only on COUNTER_TYPE")
};
let key = crate::eval::literals::value_to_key(index)?;
let Some(val) = map.shift_remove(&key) else {
return Err(crate::value::ExceptionValue::key_error(&key).into());
};
let freed = crate::state::estimate_key_size(&key) + crate::state::estimate_value_size(&val);
Ok(-to_isize_sat(freed))
}
#[expect(clippy::unnecessary_wraps, reason = "MissingSlot protocol")]
const fn counter_missing(_container: &Value, _key: &Value) -> Result<Value, EvalError> {
Ok(Value::Int(0))
}
#[expect(clippy::unnecessary_wraps, reason = "LenSlot protocol")]
fn counter_len(value: &Value) -> Result<usize, EvalError> {
let Value::Counter(map) = value else { unreachable!("counter_len only on COUNTER_TYPE") };
Ok(map.len())
}
fn counter_get_attr(value: &Value, name: &str) -> EvalResult {
const COUNTER_METHODS: &[&str] = &[
"keys",
"values",
"items",
"get",
"pop",
"copy",
"clear",
"setdefault",
"most_common",
"elements",
"subtract",
"update",
"total",
];
if COUNTER_METHODS.contains(&name) {
return Ok(bound_method(value, name));
}
Err(attribute_error("Counter", name))
}
fn counter_arith(
op: BinOp,
lhs: &Value,
rhs: &Value,
_decimal_prec: i64,
) -> Option<Result<Value, EvalError>> {
let Value::Counter(a) = lhs else { return None };
let Value::Counter(b) = rhs else { return None };
match op {
BinOp::Add => Some(Ok(Value::Counter(counter_combine_op(a, b, |x, y| x + y)))),
BinOp::Sub => Some(Ok(Value::Counter(counter_combine_op(a, b, |x, y| x - y)))),
_ => None,
}
}
pub(crate) fn counter_combine_op(
a: &indexmap::IndexMap<crate::value::ValueKey, Value>,
b: &indexmap::IndexMap<crate::value::ValueKey, Value>,
op: fn(i64, i64) -> i64,
) -> indexmap::IndexMap<crate::value::ValueKey, Value> {
let mut result = indexmap::IndexMap::new();
for (key, av) in a {
let ax = counter_int(av);
let bx = b.get(key).map_or(0, counter_int);
let r = op(ax, bx);
if r > 0 {
result.insert(key.clone(), Value::Int(r));
}
}
for (key, bv) in b {
if a.contains_key(key) {
continue;
}
let r = op(0, counter_int(bv));
if r > 0 {
result.insert(key.clone(), Value::Int(r));
}
}
result
}
fn counter_int(value: &Value) -> i64 {
match value {
Value::Int(n) => *n,
Value::Bool(b) => i64::from(*b),
_ => 0,
}
}
const fn noimpl_eq(_lhs: &Value, _rhs: &Value) -> Option<bool> {
None
}
fn deque_eq(lhs: &Value, rhs: &Value) -> Option<bool> {
let (Value::Deque { items: a, .. }, Value::Deque { items: b, .. }) = (lhs, rhs) else {
return None;
};
Some(
a.len() == b.len()
&& a.iter().zip(b.iter()).all(|(x, y)| crate::eval::operations::values_equal_pub(x, y)),
)
}
#[expect(clippy::unnecessary_wraps, reason = "ContainsSlot protocol")]
fn deque_contains(container: &Value, item: &Value) -> Result<bool, EvalError> {
let Value::Deque { items, .. } = container else {
unreachable!("deque_contains only on DEQUE_TYPE")
};
Ok(items.iter().any(|entry| recurse_eq(item, entry)))
}
#[expect(clippy::unnecessary_wraps, reason = "IterSlot protocol")]
fn deque_iter(value: &Value) -> Result<Vec<Value>, EvalError> {
let Value::Deque { items, .. } = value else { unreachable!("deque_iter only on DEQUE_TYPE") };
Ok(items.iter().cloned().collect())
}
fn deque_get_item(container: &Value, index: &Value) -> Result<Value, EvalError> {
let Value::Deque { items, .. } = container else {
unreachable!("deque_get_item only on DEQUE_TYPE")
};
let raw = int_index(index, "deque")?;
let idx = normalize_seq_index(raw, items.len(), "deque")?;
Ok(items[idx].clone())
}
fn deque_set_item(container: &mut Value, index: &Value, value: Value) -> Result<isize, EvalError> {
let Value::Deque { items, .. } = container else {
unreachable!("deque_set_item only on DEQUE_TYPE")
};
let raw = int_index(index, "deque")?;
let idx = normalize_seq_index(raw, items.len(), "deque")?;
let new_size = crate::state::estimate_value_size(&value);
let old = std::mem::replace(&mut items[idx], value);
Ok(crate::eval::place::size_delta(crate::state::estimate_value_size(&old), new_size))
}
fn deque_del_item(container: &mut Value, index: &Value) -> Result<isize, EvalError> {
let Value::Deque { items, .. } = container else {
unreachable!("deque_del_item only on DEQUE_TYPE")
};
let raw = int_index(index, "deque")?;
let idx = normalize_seq_index(raw, items.len(), "deque")?;
let removed = items.remove(idx);
Ok(-crate::eval::place::to_isize(removed.as_ref().map_or(0, crate::state::estimate_value_size)))
}
#[expect(clippy::unnecessary_wraps, reason = "LenSlot protocol")]
fn deque_len(value: &Value) -> Result<usize, EvalError> {
let Value::Deque { items, .. } = value else { unreachable!("deque_len only on DEQUE_TYPE") };
Ok(items.len())
}
fn deque_get_attr(value: &Value, name: &str) -> EvalResult {
const DEQUE_METHODS: &[&str] = &[
"append",
"appendleft",
"pop",
"popleft",
"extend",
"extendleft",
"rotate",
"clear",
"copy",
"index",
"count",
"insert",
"remove",
"reverse",
];
if DEQUE_METHODS.contains(&name) {
return Ok(bound_method(value, name));
}
if name == "maxlen" {
let Value::Deque { maxlen, .. } = value else {
unreachable!("deque_get_attr only on DEQUE_TYPE")
};
return Ok(maxlen.map_or(Value::None, |n| Value::Int(i64::try_from(n).unwrap_or(i64::MAX))));
}
Err(attribute_error("deque", name))
}
fn defaultdict_contains(container: &Value, item: &Value) -> Result<bool, EvalError> {
let Value::DefaultDict(data) = container else {
unreachable!("defaultdict_contains only on DEFAULTDICT_TYPE")
};
let key = crate::eval::literals::value_to_key(item)?;
Ok(data.items.contains_key(&key))
}
#[expect(clippy::unnecessary_wraps, reason = "IterSlot protocol")]
fn defaultdict_iter(value: &Value) -> Result<Vec<Value>, EvalError> {
let Value::DefaultDict(data) = value else {
unreachable!("defaultdict_iter only on DEFAULTDICT_TYPE")
};
Ok(data.items.keys().map(crate::value::ValueKey::to_value).collect())
}
fn defaultdict_set_item(
container: &mut Value,
index: &Value,
value: Value,
) -> Result<isize, EvalError> {
let Value::DefaultDict(data) = container else {
unreachable!("defaultdict_set_item only on DEFAULTDICT_TYPE")
};
let key = crate::eval::literals::value_to_key(index)?;
let new_size = crate::state::estimate_value_size(&value);
let delta = data.items.insert(key.clone(), value).map_or_else(
|| to_isize_sat(crate::state::estimate_key_size(&key) + new_size),
|old| size_delta(crate::state::estimate_value_size(&old), new_size),
);
Ok(delta)
}
fn defaultdict_del_item(container: &mut Value, index: &Value) -> Result<isize, EvalError> {
let Value::DefaultDict(data) = container else {
unreachable!("defaultdict_del_item only on DEFAULTDICT_TYPE")
};
let key = crate::eval::literals::value_to_key(index)?;
let Some(val) = data.items.shift_remove(&key) else {
return Err(crate::value::ExceptionValue::key_error(&key).into());
};
let freed = crate::state::estimate_key_size(&key) + crate::state::estimate_value_size(&val);
Ok(-to_isize_sat(freed))
}
#[expect(clippy::unnecessary_wraps, reason = "LenSlot protocol")]
fn defaultdict_len(value: &Value) -> Result<usize, EvalError> {
let Value::DefaultDict(data) = value else {
unreachable!("defaultdict_len only on DEFAULTDICT_TYPE")
};
Ok(data.items.len())
}
fn decimal_to_bigdecimal(value: &Value) -> Option<bigdecimal::BigDecimal> {
match value {
Value::Decimal(d, _) => Some((**d).clone()),
Value::Int(i) => Some(bigdecimal::BigDecimal::from(*i)),
Value::BigInt(i) => Some(bigdecimal::BigDecimal::from(i.as_ref().clone())),
Value::Bool(b) => Some(bigdecimal::BigDecimal::from(i64::from(*b))),
_ => None,
}
}
fn decimal_eq(lhs: &Value, rhs: &Value) -> Option<bool> {
use crate::value::DecimalKind as K;
let (ka, kb) = (decimal_operand_kind(lhs), decimal_operand_kind(rhs));
if ka.is_special() || kb.is_special() {
if ka.is_nan() || kb.is_nan() {
return Some(false);
}
return Some(matches!((ka, kb), (K::PosInf, K::PosInf) | (K::NegInf, K::NegInf)));
}
if matches!(rhs, Value::Float(_) | Value::Fraction(_)) {
return Some(decimal_to_bigrational(lhs)? == exact_rational(rhs)?);
}
Some(decimal_to_bigdecimal(lhs)? == decimal_to_bigdecimal(rhs)?)
}
fn decimal_lt(lhs: &Value, rhs: &Value) -> Option<Result<bool, EvalError>> {
let (ka, kb) = (decimal_operand_kind(lhs), decimal_operand_kind(rhs));
if ka.is_special() || kb.is_special() {
if ka.is_nan() || kb.is_nan() {
return Some(Err(EvalError::Exception(crate::value::ExceptionValue::new(
"InvalidOperation",
"comparison involving NaN",
))));
}
let rank = |k: crate::value::DecimalKind| match k {
crate::value::DecimalKind::NegInf => -2_i32,
crate::value::DecimalKind::PosInf => 2,
_ => 0,
};
return Some(Ok(rank(ka) < rank(kb)));
}
let mixed = (matches!(lhs, Value::Decimal(..))
&& matches!(rhs, Value::Float(_) | Value::Fraction(..)))
|| (matches!(rhs, Value::Decimal(..))
&& matches!(lhs, Value::Float(_) | Value::Fraction(..)));
if mixed {
return Some(Ok(tower_partial_cmp(lhs, rhs) == Some(std::cmp::Ordering::Less)));
}
Some(Ok(decimal_to_bigdecimal(lhs)? < decimal_to_bigdecimal(rhs)?))
}
fn decimal_arith(
op: BinOp,
lhs: &Value,
rhs: &Value,
decimal_prec: i64,
) -> Option<Result<Value, EvalError>> {
use num_traits::Zero as _;
if matches!(lhs, Value::Float(_)) || matches!(rhs, Value::Float(_)) {
return Some(Err(InterpreterError::TypeError(
"unsupported operand type(s) for arithmetic: 'Decimal' and 'float'".into(),
)
.into()));
}
if decimal_operand_kind(lhs).is_special() || decimal_operand_kind(rhs).is_special() {
return Some(decimal_special_arith(op, lhs, rhs));
}
let (a, b) = (decimal_to_bigdecimal(lhs)?, decimal_to_bigdecimal(rhs)?);
let result: bigdecimal::BigDecimal = match op {
BinOp::Add => a + b,
BinOp::Sub => a - b,
BinOp::Mul => a * b,
BinOp::Div => {
if b.is_zero() {
return Some(Err(
InterpreterError::Runtime("Decimal division by zero".into()).into()
));
}
let prec = decimal_prec;
let digits = u64::try_from(prec).unwrap_or(28);
let q = a / b;
if q.digits() > digits { q.with_prec(digits) } else { q }
}
BinOp::FloorDiv => {
if b.is_zero() {
return Some(Err(
InterpreterError::Runtime("Decimal division by zero".into()).into()
));
}
(a / b).with_scale(0)
}
BinOp::Mod => {
if b.is_zero() {
return Some(Err(
InterpreterError::Runtime("Decimal division by zero".into()).into()
));
}
let q = (a.clone() / b.clone()).with_scale(0);
a - q * b
}
BinOp::Pow => {
use num_traits::ToPrimitive as _;
let exp = (b.fractional_digit_count() <= 0).then(|| b.to_i64()).flatten()?;
let mut acc = bigdecimal::BigDecimal::from(1);
for _ in 0..exp.unsigned_abs() {
acc *= &a;
}
if exp < 0 {
if a.is_zero() {
return Some(Err(
InterpreterError::Runtime("Decimal division by zero".into()).into()
));
}
let digits = u64::try_from(decimal_prec).unwrap_or(28);
let inv = bigdecimal::BigDecimal::from(1) / acc;
if inv.digits() > digits { inv.with_prec(digits) } else { inv }
} else {
acc
}
}
};
Some(Ok(Value::Decimal(Box::new(result), crate::value::DecimalKind::Normal)))
}
fn decimal_operand_kind(v: &Value) -> crate::value::DecimalKind {
match v {
Value::Decimal(_, k) => *k,
_ => crate::value::DecimalKind::Normal,
}
}
fn decimal_operand_negative(v: &Value) -> bool {
use num_traits::Signed as _;
match v {
Value::Decimal(d, k) => {
matches!(k, crate::value::DecimalKind::NegInf | crate::value::DecimalKind::NegZero)
|| d.is_negative()
}
Value::Int(i) => *i < 0,
Value::BigInt(b) => b.is_negative(),
_ => false,
}
}
fn decimal_special_arith(op: BinOp, lhs: &Value, rhs: &Value) -> Result<Value, EvalError> {
use crate::value::DecimalKind as K;
use num_traits::Zero as _;
let mk = |k: K| Ok(Value::Decimal(Box::new(bigdecimal::BigDecimal::from(0)), k));
let invalid = || {
Err(EvalError::Exception(crate::value::ExceptionValue::new(
"InvalidOperation",
"[<class 'decimal.InvalidOperation'>]",
)))
};
let (ka, kb) = (decimal_operand_kind(lhs), decimal_operand_kind(rhs));
let (na, nb) = (decimal_operand_negative(lhs), decimal_operand_negative(rhs));
if ka.is_nan() || kb.is_nan() {
return mk(K::Nan);
}
let inf = |neg: bool| if neg { K::NegInf } else { K::PosInf };
let a_zero = matches!(lhs, Value::Decimal(d, k) if !k.is_special() && d.is_zero());
let b_zero = matches!(rhs, Value::Decimal(d, k) if !k.is_special() && d.is_zero());
match op {
BinOp::Add => match (ka.is_infinite(), kb.is_infinite()) {
(true, true) => {
if na == nb {
mk(inf(na))
} else {
invalid() }
}
(true, false) => mk(inf(na)),
(false, true) => mk(inf(nb)),
(false, false) => mk(K::Normal),
},
BinOp::Sub => match (ka.is_infinite(), kb.is_infinite()) {
(true, true) => {
if na != nb {
mk(inf(na))
} else {
invalid() }
}
(true, false) => mk(inf(na)),
(false, true) => mk(inf(!nb)),
(false, false) => mk(K::Normal),
},
BinOp::Mul => {
if (ka.is_infinite() && b_zero) || (kb.is_infinite() && a_zero) {
return invalid(); }
if ka.is_infinite() || kb.is_infinite() {
return mk(inf(na != nb));
}
mk(K::Normal)
}
BinOp::Div => match (ka.is_infinite(), kb.is_infinite()) {
(true, true) => invalid(), (true, false) => mk(inf(na != nb)), (false, true) => {
let zero = bigdecimal::BigDecimal::new(num_bigint::BigInt::from(0), 1_000_026);
let sign = if na != nb { K::NegZero } else { K::Normal };
Ok(Value::Decimal(Box::new(zero), sign))
}
(false, false) => mk(K::Normal),
},
_ => invalid(),
}
}
fn fraction_to_bigrational(value: &Value) -> Option<num_rational::BigRational> {
use num_bigint::BigInt;
match value {
Value::Fraction(f) => Some((**f).clone()),
Value::Int(i) => Some(num_rational::BigRational::from_integer(BigInt::from(*i))),
Value::BigInt(i) => Some(num_rational::BigRational::from_integer(i.as_ref().clone())),
Value::Bool(b) => {
Some(num_rational::BigRational::from_integer(BigInt::from(i64::from(*b))))
}
_ => None,
}
}
fn float_to_bigrational(f: f64) -> Option<num_rational::BigRational> {
use num_bigint::BigInt;
use num_traits::Float as _;
if !f.is_finite() {
return None;
}
let (mantissa, exp, sign) = f.integer_decode();
let numer = BigInt::from(mantissa) * BigInt::from(i64::from(sign));
if exp >= 0 {
Some(num_rational::BigRational::from_integer(numer << usize::try_from(exp).ok()?))
} else {
Some(num_rational::BigRational::new(numer, BigInt::from(1) << usize::try_from(-exp).ok()?))
}
}
fn decimal_to_bigrational(value: &Value) -> Option<num_rational::BigRational> {
use num_bigint::BigInt;
let Value::Decimal(d, _) = value else { return None };
let (mantissa, scale) = d.as_bigint_and_exponent();
let ten = BigInt::from(10);
if scale >= 0 {
Some(num_rational::BigRational::new(mantissa, ten.pow(u32::try_from(scale).ok()?)))
} else {
Some(num_rational::BigRational::from_integer(
mantissa * ten.pow(u32::try_from(-scale).ok()?),
))
}
}
fn exact_rational(value: &Value) -> Option<num_rational::BigRational> {
match value {
Value::Float(f) => float_to_bigrational(*f),
Value::Decimal(..) => decimal_to_bigrational(value),
_ => fraction_to_bigrational(value),
}
}
fn tower_partial_cmp(lhs: &Value, rhs: &Value) -> Option<std::cmp::Ordering> {
if matches!(lhs, Value::Float(f) if f.is_nan()) || matches!(rhs, Value::Float(f) if f.is_nan())
{
return None;
}
let rank = |v: &Value| match v {
Value::Float(f) if f.is_infinite() => {
if *f > 0.0 {
1
} else {
-1
}
}
_ => 0,
};
match (rank(lhs), rank(rhs)) {
(0, 0) => Some(exact_rational(lhs)?.cmp(&exact_rational(rhs)?)),
(a, b) => Some(a.cmp(&b)),
}
}
fn fraction_eq(lhs: &Value, rhs: &Value) -> Option<bool> {
let a = fraction_to_bigrational(lhs)?;
if matches!(rhs, Value::Float(_) | Value::Decimal(..)) {
return Some(exact_rational(rhs).is_some_and(|b| a == b));
}
Some(a == fraction_to_bigrational(rhs)?)
}
fn fraction_lt(lhs: &Value, rhs: &Value) -> Option<Result<bool, EvalError>> {
let mixed = (matches!(lhs, Value::Fraction(..))
&& matches!(rhs, Value::Float(_) | Value::Decimal(..)))
|| (matches!(rhs, Value::Fraction(..))
&& matches!(lhs, Value::Float(_) | Value::Decimal(..)));
if mixed {
return Some(Ok(tower_partial_cmp(lhs, rhs) == Some(std::cmp::Ordering::Less)));
}
Some(Ok(fraction_to_bigrational(lhs)? < fraction_to_bigrational(rhs)?))
}
fn fraction_to_f64(value: &Value) -> Option<f64> {
use num_traits::ToPrimitive as _;
match value {
Value::Float(f) => Some(*f),
Value::Fraction(f) => f.to_f64(),
Value::Int(i) => Some(*i as f64),
Value::BigInt(i) => i.to_f64(),
Value::Bool(b) => Some(f64::from(*b)),
_ => None,
}
}
fn fraction_arith(
op: BinOp,
lhs: &Value,
rhs: &Value,
_decimal_prec: i64,
) -> Option<Result<Value, EvalError>> {
if matches!(lhs, Value::Float(_)) || matches!(rhs, Value::Float(_)) {
let a = fraction_to_f64(lhs)?;
let b = fraction_to_f64(rhs)?;
let result = match op {
BinOp::Add => a + b,
BinOp::Sub => a - b,
BinOp::Mul => a * b,
BinOp::Div => a / b,
BinOp::FloorDiv => (a / b).floor(),
BinOp::Mod => a % b,
BinOp::Pow => a.powf(b),
};
return Some(Ok(Value::Float(result)));
}
let (a, b) = (fraction_to_bigrational(lhs)?, fraction_to_bigrational(rhs)?);
let result: num_rational::BigRational = match op {
BinOp::Add => a + b,
BinOp::Sub => a - b,
BinOp::Mul => a * b,
BinOp::Div => {
if b.numer().sign() == num_bigint::Sign::NoSign {
return Some(Err(
InterpreterError::Runtime("Fraction division by zero".into()).into()
));
}
a / b
}
BinOp::FloorDiv => {
if b.numer().sign() == num_bigint::Sign::NoSign {
return Some(Err(
InterpreterError::Runtime("Fraction division by zero".into()).into()
));
}
let floored = (a / b).floor();
return Some(Ok(crate::value::int_from_bigint(floored.to_integer())));
}
BinOp::Mod => {
if b.numer().sign() == num_bigint::Sign::NoSign {
return Some(Err(
InterpreterError::Runtime("Fraction division by zero".into()).into()
));
}
let q = (a.clone() / b.clone()).floor();
a - q * b
}
BinOp::Pow => {
use num_traits::ToPrimitive as _;
if !b.is_integer() {
let base = fraction_to_f64(lhs)?;
let exp = fraction_to_f64(rhs)?;
return Some(Ok(Value::Float(base.powf(exp))));
}
let exp = b.numer().to_i32()?;
if exp < 0 && a.numer().sign() == num_bigint::Sign::NoSign {
return Some(Err(
InterpreterError::Runtime("Fraction division by zero".into()).into()
));
}
a.pow(exp)
}
};
Some(Ok(Value::Fraction(Box::new(result))))
}
fn fraction_get_attr(value: &Value, name: &str) -> EvalResult {
let Value::Fraction(f) = value else { unreachable!("fraction_get_attr only on FRACTION_TYPE") };
match name {
"numerator" => Ok(bigint_to_value(f.numer())),
"denominator" => Ok(bigint_to_value(f.denom())),
_ => Err(InterpreterError::AttributeError(format!(
"'Fraction' object has no attribute '{name}'"
))
.into()),
}
}
fn bigint_to_value(value: &num_bigint::BigInt) -> Value {
crate::value::int_from_bigint(value.clone())
}
const fn binop_to_sym(op: BinOp) -> &'static str {
match op {
BinOp::Add => "+",
BinOp::Sub => "-",
BinOp::Mul => "*",
BinOp::FloorDiv => "//",
BinOp::Div => "/",
BinOp::Mod => "%",
_ => "",
}
}
fn datetime_cluster_arith(
op: BinOp,
lhs: &Value,
rhs: &Value,
_decimal_prec: i64,
) -> Option<Result<Value, EvalError>> {
let sym = binop_to_sym(op);
if sym.is_empty() {
return None;
}
crate::eval::modules::datetime::try_arith(sym, lhs, rhs)
}
fn date_get_attr(value: &Value, name: &str) -> EvalResult {
let Value::Date(d) = value else { unreachable!("date_get_attr only on DATE_TYPE") };
crate::eval::modules::datetime::date_attribute(*d, name)
}
fn datetime_get_attr(value: &Value, name: &str) -> EvalResult {
let Value::DateTime { dt, tz_offset_secs } = value else {
unreachable!("datetime_get_attr only on DATETIME_TYPE")
};
crate::eval::modules::datetime::datetime_attribute(*dt, *tz_offset_secs, name)
}
fn time_get_attr(value: &Value, name: &str) -> EvalResult {
let Value::Time(t) = value else { unreachable!("time_get_attr only on TIME_TYPE") };
crate::eval::modules::datetime::time_attribute(*t, name)
}
fn timedelta_get_attr(value: &Value, name: &str) -> EvalResult {
let Value::TimeDelta(micros) = value else {
unreachable!("timedelta_get_attr only on TIMEDELTA_TYPE")
};
crate::eval::modules::datetime::timedelta_attribute(*micros, name)
}
fn hashdigest_get_attr(value: &Value, name: &str) -> EvalResult {
let Value::HashDigest { algo, bytes } = value else {
unreachable!("hashdigest_get_attr only on HASHDIGEST_TYPE")
};
crate::eval::modules::hashlib::hash_attribute(algo, bytes, name)
}
fn enummember_contains(container: &Value, item: &Value) -> Result<bool, EvalError> {
let Value::EnumMember { kind, value: cv, .. } = container else {
unreachable!("enummember_contains only on ENUMMEMBER_TYPE")
};
if !kind.is_flag() {
return Err(
InterpreterError::TypeError("argument of type 'enum' is not iterable".into()).into()
);
}
let container_bits = crate::value::value_as_i64(cv).unwrap_or(0);
let item_bits = match item {
Value::EnumMember { value, .. } => crate::value::value_as_i64(value).unwrap_or(-1),
_ => {
return Err(InterpreterError::TypeError(format!(
"unsupported operand type(s) for 'in': '{}' and 'enum'",
item.type_name()
))
.into());
}
};
Ok(item_bits >= 0 && container_bits & item_bits == item_bits)
}
fn enummember_get_attr(value: &Value, name: &str) -> EvalResult {
let Value::EnumMember { class_name, member_name, value: inner, .. } = value else {
unreachable!("enummember_get_attr only on ENUMMEMBER_TYPE")
};
match name {
"name" => Ok(Value::String(member_name.clone().into())),
"value" => Ok((**inner).clone()),
_ => Err(InterpreterError::AttributeError(format!(
"'{class_name}.{member_name}' enum member has no attribute '{name}'"
))
.into()),
}
}
pub fn type_error_unsupported(op: &str, lhs: &Value, rhs: &Value) -> EvalError {
let type_name = |v: &Value| match v {
Value::Instance(inst) => inst.class_name.clone(),
other => type_of(other).name.to_string(),
};
InterpreterError::TypeError(format!(
"'{op}' not supported between instances of '{}' and '{}'",
type_name(lhs),
type_name(rhs),
))
.into()
}