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) -> i64;
pub type LtSlot = fn(lhs: &Value, rhs: &Value) -> Option<bool>;
pub type ContainsSlot = fn(container: &Value, item: &Value) -> Result<bool, EvalError>;
pub type ArithSlot = fn(op: BinOp, lhs: &Value, rhs: &Value) -> 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> {
type_of(value).get_attr_slot.map_or_else(|| Ok(None), |slot| slot(value, name).map(Some))
}
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> {
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> {
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> {
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> {
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) -> 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);
}
let lhs_type = type_of(lhs);
if let Some(result) = (lhs_type.arith_slot)(op, lhs, rhs) {
return result;
}
let rhs_type = type_of(rhs);
if let Some(result) = (rhs_type.arith_slot)(op, lhs, rhs) {
return result;
}
Err(InterpreterError::TypeError(format!(
"unsupported operand type(s) for {}: '{}' and '{}'",
op.symbol(),
lhs_type.name,
rhs_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 Ok(result);
}
let rhs_type = type_of(rhs);
if let Some(result) = (rhs_type.lt_slot)(lhs, rhs) {
return Ok(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> {
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::Instance(inst) = value {
if let Some(class) = state.classes.get(&inst.class_name) {
if class.dataclass_fields.is_some() && !class.methods.contains_key("__hash__") {
return Err(InterpreterError::TypeError(format!(
"unhashable type: '{}'",
inst.class_name
))
.into());
}
}
}
let type_obj = type_of(value);
type_obj.hash_slot.map_or_else(
|| Err(InterpreterError::TypeError(format!("unhashable type: '{}'", type_obj.name)).into()),
|slot| Ok(slot(value)),
)
}
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__") {
let mut equal = true;
let af = inst.fields.lock();
let bf = other_inst.fields.lock();
for field in fields.iter().filter(|f| f.compare) {
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));
}
}
}
}
}
return Ok(Value::Bool(
matches!(rhs, Value::Instance(other_inst) if std::ptr::eq(inst, other_inst)),
));
}
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::String(_) => &STR_TYPE,
Value::Bytes(_) => &BYTES_TYPE,
Value::List(_) => &LIST_TYPE,
Value::Tuple(_) => &TUPLE_TYPE,
Value::Dict(_) => &DICT_TYPE,
Value::Set(_) => &SET_TYPE,
Value::Range { .. } => &RANGE_TYPE,
Value::Counter(_) => &COUNTER_TYPE,
Value::Deque { .. } => &DEQUE_TYPE,
Value::DefaultDict { .. } => &DEFAULTDICT_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(noattr_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(noattr_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(noattr_get_attr),
set_attr_slot: None,
has_methods_table: false,
};
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: None,
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(noattr_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 SET_TYPE: TypeObject = TypeObject {
name: "set",
eq_slot: set_eq,
hash_slot: None,
lt_slot: noimpl_lt,
contains_slot: Some(sequence_contains),
arith_slot: set_arith,
iter_slot: Some(sequence_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 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(noattr_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: noimpl_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: None,
del_item_slot: None,
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 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: 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(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: noimpl_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: noimpl_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: 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(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: noimpl_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: None,
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: None,
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)
}
fn bytes_eq(lhs: &Value, rhs: &Value) -> Option<bool> {
let Value::Bytes(a) = lhs else { return None };
let Value::Bytes(b) = rhs else { return None };
Some(a == b)
}
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_guard = a.lock();
let b_guard = b.lock();
Some(elementwise_eq(&a_guard, &b_guard))
}
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 Value::Dict(a) = lhs else { return None };
let Value::Dict(b) = rhs else { return None };
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 set_eq(lhs: &Value, rhs: &Value) -> Option<bool> {
let Value::Set(a) = lhs else { return None };
let Value::Set(b) = rhs else { return None };
if a.len() != b.len() {
return Some(false);
}
let equal = a.iter().all(|av| b.iter().any(|bv| recurse_eq(av, bv)));
Some(equal)
}
#[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))
}
fn recurse_eq(lhs: &Value, rhs: &Value) -> bool {
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<bool> {
None
}
fn bool_lt(lhs: &Value, rhs: &Value) -> Option<bool> {
let Value::Bool(a) = lhs else { return None };
let av = i64::from(*a);
match rhs {
Value::Bool(b) => Some(av < i64::from(*b)),
Value::Int(b) => Some(av < *b),
#[expect(
clippy::cast_precision_loss,
reason = "Python bool↔float compare matches CPython's lossy compare"
)]
Value::Float(b) => Some((av as f64) < *b),
_ => None,
}
}
fn int_lt(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(b) => {
use num_traits::ToPrimitive as _;
Some(a.to_f64().is_some_and(|af| af < *b))
}
_ => None,
}
}
fn float_lt(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 }),
#[expect(
clippy::cast_precision_loss,
reason = "Python int↔float compare matches CPython's lossy compare"
)]
Value::Int(b) => Some(*a < (*b as f64)),
Value::BigInt(b) => {
use num_traits::ToPrimitive as _;
Some(b.to_f64().is_some_and(|bf| *a < bf))
}
_ => None,
}
}
fn str_lt(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)
}
fn bytes_lt(lhs: &Value, rhs: &Value) -> Option<bool> {
let Value::Bytes(a) = lhs else { return None };
let Value::Bytes(b) = rhs else { return None };
Some(a < b)
}
fn list_lt(lhs: &Value, rhs: &Value) -> Option<bool> {
let Value::List(a) = lhs else { return None };
let Value::List(b) = rhs else { return None };
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<bool> {
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]) -> bool {
for (x, y) in a.iter().zip(b.iter()) {
if !recurse_eq(x, y) {
return dispatch_lt(x, y).unwrap_or(false);
}
}
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);
}
let (Value::Tuple(items) | Value::Set(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)
}
#[expect(
clippy::unnecessary_wraps,
reason = "ContainsSlot protocol fixes the Result signature; see sequence_contains rationale"
)]
fn dict_contains(container: &Value, item: &Value) -> Result<bool, EvalError> {
let Value::Dict(map) = container else {
unreachable!("dict_contains only attached to DICT_TYPE")
};
let Ok(key) = crate::eval::literals::value_to_key(item) else {
return Ok(false);
};
Ok(map.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 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) -> Option<Result<Value, EvalError>> {
None
}
fn numeric_arith(op: BinOp, lhs: &Value, rhs: &Value) -> 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) -> 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) -> Option<Result<Value, EvalError>> {
let Value::Bytes(_) = lhs else { return None };
match op {
BinOp::Add if matches!(rhs, Value::Bytes(_)) => {
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 list_arith(op: BinOp, lhs: &Value, rhs: &Value) -> 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 tuple_arith(op: BinOp, lhs: &Value, rhs: &Value) -> 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) -> Option<Result<Value, EvalError>> {
let Value::Set(_) = lhs else { return None };
match op {
BinOp::Sub if matches!(rhs, Value::Set(_)) => {
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::Set(items)) = value else {
unreachable!("sequence_iter only attached to list/tuple/set TypeObjects")
};
Ok(items.clone())
}
#[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 Value::Bytes(b) = value else { unreachable!("bytes_iter only on BYTES_TYPE") };
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 Value::Dict(map) = value else { unreachable!("dict_iter only on DICT_TYPE") };
Ok(map.keys().map(crate::value::ValueKey::to_value).collect())
}
#[expect(
clippy::unnecessary_wraps,
reason = "IterSlot protocol; range walk cannot fail (step != 0 is enforced at construction)"
)]
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 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 }
}
const fn none_hash(_value: &Value) -> i64 {
0
}
fn bool_hash(value: &Value) -> i64 {
let Value::Bool(b) = value else { unreachable!("bool_hash sees only Value::Bool") };
finalize_hash(int_hash_impl(i64::from(*b)))
}
fn int_hash_slot(value: &Value) -> i64 {
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) -> i64 {
let Value::Float(f) = value else { unreachable!("float_hash_slot sees only Value::Float") };
finalize_hash(float_hash_impl(*f))
}
#[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) -> i64 {
use std::hash::{Hash as _, Hasher as _};
let Ok(key) = crate::eval::literals::value_to_key(value) else {
return 0;
};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
key.hash(&mut hasher);
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 Value::Bytes(b) = container else { unreachable!("bytes_get_item only on BYTES_TYPE") };
let raw = int_index(index, "bytes")?;
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 Value::Dict(map) = container else { unreachable!("dict_get_item only on DICT_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 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 Value::Dict(map) = container else { unreachable!("dict_set_item only on DICT_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 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") };
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 Value::Dict(map) = container else { unreachable!("dict_del_item only on DICT_TYPE") };
let key = crate::eval::literals::value_to_key(index)?;
let Some(val) = map.swap_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)),
other => Err(InterpreterError::TypeError(format!(
"{container_name} indices must be integers, not '{}'",
other.type_name()
))
.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());
}
let (Value::Tuple(items) | Value::Set(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())
}
#[expect(clippy::unnecessary_wraps, reason = "LenSlot protocol")]
fn bytes_len(value: &Value) -> Result<usize, EvalError> {
let Value::Bytes(b) = value else { unreachable!("bytes_len only on BYTES_TYPE") };
Ok(b.len())
}
#[expect(clippy::unnecessary_wraps, reason = "LenSlot protocol")]
fn dict_len(value: &Value) -> Result<usize, EvalError> {
let Value::Dict(map) = value else { unreachable!("dict_len only on DICT_TYPE") };
Ok(map.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))
}
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", "update", "setdefault", "copy", "clear"];
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",
"encode",
];
const LIST_METHODS: &[&str] = &[
"append", "extend", "insert", "pop", "remove", "sort", "reverse", "index", "count", "copy",
"clear",
];
const TUPLE_METHODS: &[&str] = &["count", "index"];
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 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))
}
fn counter_eq(lhs: &Value, rhs: &Value) -> Option<bool> {
let Value::Counter(a) = lhs else { return None };
match rhs {
Value::Counter(b) | Value::Dict(b) => {
if a.len() != b.len() {
return Some(false);
}
Some(a.iter().all(|(k, v)| b.get(k).is_some_and(|bv| recurse_eq(v, bv))))
}
_ => None,
}
}
#[expect(clippy::unnecessary_wraps, reason = "ContainsSlot protocol")]
fn counter_contains(container: &Value, item: &Value) -> Result<bool, EvalError> {
let Value::Counter(map) = container else {
unreachable!("counter_contains only on COUNTER_TYPE")
};
let Ok(key) = crate::eval::literals::value_to_key(item) else {
return Ok(false);
};
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.swap_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) -> 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
}
#[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())
}
#[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",
];
if DEQUE_METHODS.contains(&name) {
return Ok(bound_method(value, name));
}
Err(attribute_error("deque", name))
}
#[expect(clippy::unnecessary_wraps, reason = "ContainsSlot protocol")]
fn defaultdict_contains(container: &Value, item: &Value) -> Result<bool, EvalError> {
let Value::DefaultDict(data) = container else {
unreachable!("defaultdict_contains only on DEFAULTDICT_TYPE")
};
let Ok(key) = crate::eval::literals::value_to_key(item) else {
return Ok(false);
};
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.swap_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> {
Some(decimal_to_bigdecimal(lhs)? == decimal_to_bigdecimal(rhs)?)
}
fn decimal_lt(lhs: &Value, rhs: &Value) -> Option<bool> {
Some(decimal_to_bigdecimal(lhs)? < decimal_to_bigdecimal(rhs)?)
}
fn decimal_arith(op: BinOp, lhs: &Value, rhs: &Value) -> 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()));
}
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 = crate::eval::modules::decimal::active_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)
}
_ => return None,
};
Some(Ok(Value::Decimal(Box::new(result))))
}
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 fraction_eq(lhs: &Value, rhs: &Value) -> Option<bool> {
Some(fraction_to_bigrational(lhs)? == fraction_to_bigrational(rhs)?)
}
fn fraction_lt(lhs: &Value, rhs: &Value) -> Option<bool> {
Some(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) -> 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()
));
}
(a / b).floor()
}
BinOp::Mod | BinOp::Pow => return None,
};
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 {
use num_traits::ToPrimitive as _;
value.to_i64().map_or_else(|| value.to_f64().map_or(Value::None, Value::Float), Value::Int)
}
const fn binop_to_sym(op: BinOp) -> &'static str {
match op {
BinOp::Add => "+",
BinOp::Sub => "-",
BinOp::Mul => "*",
BinOp::FloorDiv => "//",
_ => "",
}
}
fn datetime_cluster_arith(op: BinOp, lhs: &Value, rhs: &Value) -> 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_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 {
InterpreterError::TypeError(format!(
"'{op}' not supported between instances of '{}' and '{}'",
type_of(lhs).name,
type_of(rhs).name,
))
.into()
}