use std::sync::Arc;
use indexmap::IndexMap;
use super::{methods, resolve_proxy};
use crate::{
error::{EvalError, InterpreterError},
eval::place,
value::{Value, shared_dict, shared_list},
};
#[derive(Clone, Copy)]
pub(crate) struct CallArgs<'a> {
pub positional: &'a [Value],
pub keyword: &'a IndexMap<String, Value>,
}
pub(crate) struct MethodOutcome {
pub value: Value,
pub mem_delta: isize,
}
impl MethodOutcome {
pub(crate) const fn pure(value: Value) -> Self {
Self { value, mem_delta: 0 }
}
pub(crate) fn grew(value: Value, bytes: usize) -> Self {
Self { value, mem_delta: place::to_isize(bytes) }
}
pub(crate) fn shrank(value: Value, bytes: usize) -> Self {
Self { value, mem_delta: -place::to_isize(bytes) }
}
}
pub(crate) fn reject_kwargs(
method: &str,
kwargs: &IndexMap<String, Value>,
) -> Result<(), EvalError> {
if let Some((name, _)) = kwargs.first() {
return Err(InterpreterError::TypeError(format!(
"{method}() got an unexpected keyword argument '{name}'"
))
.into());
}
Ok(())
}
pub(crate) fn bind_method_params(
method: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
params: &[&str],
) -> Result<Vec<Option<Value>>, EvalError> {
if args.len() > params.len() {
return Err(InterpreterError::TypeError(format!(
"{method}() takes at most {} argument{} ({} given)",
params.len(),
if params.len() == 1 { "" } else { "s" },
args.len()
))
.into());
}
let mut bound: Vec<Option<Value>> = params.iter().map(|_| None).collect();
for (i, arg) in args.iter().enumerate() {
bound[i] = Some(arg.clone());
}
for (name, value) in kwargs {
let Some(idx) = params.iter().position(|p| *p == name.as_str()) else {
return Err(InterpreterError::TypeError(format!(
"{method}() got an unexpected keyword argument '{name}'"
))
.into());
};
if bound[idx].is_some() {
return Err(InterpreterError::TypeError(format!(
"{method}() got multiple values for argument '{name}'"
))
.into());
}
bound[idx] = Some(value.clone());
}
Ok(bound)
}
pub(crate) fn require_param<'a>(
method: &str,
bound: &'a [Option<Value>],
idx: usize,
name: &str,
) -> Result<&'a Value, EvalError> {
bound.get(idx).and_then(Option::as_ref).ok_or_else(|| {
EvalError::from(InterpreterError::TypeError(format!(
"{method}() missing required argument: '{name}'"
)))
})
}
pub(super) async fn resolve_method_args(args: &[Value]) -> Result<Vec<Value>, EvalError> {
let mut resolved_args = Vec::with_capacity(args.len());
for arg in args {
let resolved = resolve_proxy(arg).await?;
match resolved {
Value::List(items) => {
let snapshot = items.lock().clone();
if snapshot.iter().any(|v| matches!(v, Value::LazyProxy(_))) {
let mut resolved_items = Vec::with_capacity(snapshot.len());
for item in &snapshot {
resolved_items.push(resolve_proxy(item).await?);
}
resolved_args.push(Value::List(shared_list(resolved_items)));
} else {
resolved_args.push(Value::List(items));
}
}
Value::Tuple(items) => {
let mut resolved_items = Vec::with_capacity(items.len());
for item in &items {
resolved_items.push(resolve_proxy(item).await?);
}
resolved_args.push(Value::Tuple(resolved_items));
}
other => resolved_args.push(other),
}
}
Ok(resolved_args)
}
pub(super) async fn resolve_method_kwargs(
kwargs: &IndexMap<String, Value>,
) -> Result<IndexMap<String, Value>, EvalError> {
let mut resolved = IndexMap::with_capacity(kwargs.len());
for (k, v) in kwargs {
resolved.insert(k.clone(), resolve_proxy(v).await?);
}
Ok(resolved)
}
type MethodsHandler =
fn(&mut Value, &str, &[Value], &IndexMap<String, Value>) -> Result<MethodOutcome, EvalError>;
fn str_methods(
obj: &mut Value,
method: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
) -> Result<MethodOutcome, EvalError> {
let Value::String(s) = obj else {
return Err(type_mismatch("str"));
};
methods::str::dispatch_string_method(s, method, args, kwargs).map(MethodOutcome::pure)
}
fn list_methods(
obj: &mut Value,
method: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
) -> Result<MethodOutcome, EvalError> {
let Value::List(items) = obj else {
return Err(type_mismatch("list"));
};
let snapped;
let args = if method == "extend"
&& args.iter().any(|a| matches!(a, Value::List(l) if Arc::ptr_eq(l, items)))
{
snapped = args
.iter()
.map(|a| match a {
Value::List(l) if Arc::ptr_eq(l, items) => {
Value::List(shared_list(l.lock().clone()))
}
other => other.clone(),
})
.collect::<Vec<_>>();
&snapped
} else {
args
};
let mut guard = items.lock();
methods::list::dispatch_list_method(&mut guard, method, args, kwargs)
}
fn range_methods(
obj: &mut Value,
method: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
) -> Result<MethodOutcome, EvalError> {
crate::eval::functions::reject_kwargs(method, kwargs)?;
let Value::Range { start, stop, step } = obj else {
return Err(type_mismatch("range"));
};
let (start, stop, step) = (*start, *stop, *step);
let position = |v: &Value| -> Option<i64> {
let n = match v {
Value::Int(n) => *n,
Value::Bool(b) => i64::from(*b),
_ => return None,
};
if step == 0 {
return None;
}
let in_bounds = if step > 0 { n >= start && n < stop } else { n <= start && n > stop };
if in_bounds && (n - start) % step == 0 { Some((n - start) / step) } else { None }
};
match method {
"index" => {
let target = arg1(method, args)?;
match position(target) {
Some(i) => Ok(MethodOutcome::pure(Value::Int(i))),
None => {
Err(InterpreterError::ValueError(format!("{target} is not in range")).into())
}
}
}
"count" => {
let n = i64::from(args.first().is_some_and(|v| position(v).is_some()));
Ok(MethodOutcome::pure(Value::Int(n)))
}
_ => Err(InterpreterError::AttributeError(format!(
"'range' object has no attribute '{method}'"
))
.into()),
}
}
fn lru_methods(
obj: &mut Value,
method: &str,
_args: &[Value],
_kwargs: &IndexMap<String, Value>,
) -> Result<MethodOutcome, EvalError> {
use std::sync::atomic::Ordering::Relaxed;
let Value::LruCache(data) = obj else {
return Err(type_mismatch("lru_cache"));
};
match method {
"cache_info" => {
let mut fields = std::collections::BTreeMap::new();
fields.insert("hits".to_string(), Value::Int(data.hits.load(Relaxed) as i64));
fields.insert("misses".to_string(), Value::Int(data.misses.load(Relaxed) as i64));
fields.insert(
"maxsize".to_string(),
data.maxsize.map_or(Value::None, |m| Value::Int(m as i64)),
);
fields.insert("currsize".to_string(), Value::Int(data.cache.lock().len() as i64));
Ok(MethodOutcome::pure(Value::Instance(crate::value::InstanceValue {
class_name: "CacheInfo".to_string(),
fields: crate::value::shared_fields(fields),
})))
}
"cache_clear" => {
data.cache.lock().clear();
data.hits.store(0, Relaxed);
data.misses.store(0, Relaxed);
Ok(MethodOutcome::pure(Value::None))
}
_ => Err(InterpreterError::AttributeError(format!(
"'functools._lru_cache_wrapper' object has no attribute '{method}'"
))
.into()),
}
}
fn array_methods(
obj: &mut Value,
method: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
) -> Result<MethodOutcome, EvalError> {
use crate::eval::modules::array_mod::{coerce_element, itemsize};
let Value::Array { typecode, items } = obj else {
return Err(type_mismatch("array.array"));
};
let typecode = *typecode;
match method {
"append" => {
let coerced = coerce_element(typecode, arg1(method, args)?)?;
let size = crate::state::estimate_value_size(&coerced);
items.lock().push(coerced);
Ok(MethodOutcome::grew(Value::None, size))
}
"extend" | "fromlist" => {
let elems = crate::eval::control_flow::iterate_value(arg1(method, args)?)?;
let mut guard = items.lock();
let mut added = 0;
for e in elems {
let c = coerce_element(typecode, &e)?;
added += crate::state::estimate_value_size(&c);
guard.push(c);
}
Ok(MethodOutcome::grew(Value::None, added))
}
"insert" => {
let idx = crate::eval::functions::value_to_i64(arg1(method, args)?)?;
let value = coerce_element(
typecode,
args.get(1).ok_or_else(|| {
EvalError::from(InterpreterError::TypeError(
"insert() takes exactly 2 arguments".into(),
))
})?,
)?;
let size = crate::state::estimate_value_size(&value);
let mut guard = items.lock();
let len = guard.len() as i64;
let pos = if idx < 0 { (len + idx).max(0) } else { idx.min(len) } as usize;
guard.insert(pos, value);
Ok(MethodOutcome::grew(Value::None, size))
}
"tolist" => {
let guard = items.lock();
Ok(MethodOutcome::pure(Value::List(crate::value::shared_list(guard.clone()))))
}
"buffer_info" => {
let len = items.lock().len() as i64;
Ok(MethodOutcome::pure(Value::Tuple(vec![Value::Int(0), Value::Int(len)])))
}
"__len__" => Ok(MethodOutcome::pure(Value::Int(crate::eval::functions::to_len_i64(
items.lock().len(),
)?))),
"pop" | "remove" | "index" | "count" | "reverse" => {
let _ = itemsize; let mut guard = items.lock();
methods::list::dispatch_list_method(&mut guard, method, args, kwargs)
}
_ => Err(InterpreterError::AttributeError(format!(
"'array.array' object has no attribute '{method}'"
))
.into()),
}
}
fn dict_methods(
obj: &mut Value,
method: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
) -> Result<MethodOutcome, EvalError> {
let Some(map) = obj.as_dict() else {
return Err(type_mismatch("dict"));
};
if let Some(kind) = match method {
"keys" => Some(crate::value::DictViewKind::Keys),
"values" => Some(crate::value::DictViewKind::Values),
"items" => Some(crate::value::DictViewKind::Items),
_ => None,
} {
reject_kwargs(method, kwargs)?;
if !args.is_empty() {
return Err(InterpreterError::TypeError(format!(
"{method}() takes no arguments ({} given)",
args.len()
))
.into());
}
return Ok(MethodOutcome::pure(Value::DictView { dict: map.clone(), kind }));
}
let snapped;
let args = if method == "update"
&& args.iter().any(|a| a.as_dict().is_some_and(|d| Arc::ptr_eq(d, map)))
{
snapped = args
.iter()
.map(|a| match a.as_dict() {
Some(d) if Arc::ptr_eq(d, map) => Value::Dict(shared_dict(d.lock().clone())),
_ => a.clone(),
})
.collect::<Vec<_>>();
&snapped
} else {
args
};
let mut guard = map.lock();
methods::dict::dispatch_dict_method(&mut guard, method, args, kwargs)
}
fn counter_methods(
obj: &mut Value,
method: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
) -> Result<MethodOutcome, EvalError> {
let Value::Counter(map) = obj else {
return Err(type_mismatch("Counter"));
};
methods::counter::dispatch_counter_method(map, method, args, kwargs)
}
fn deque_methods(
obj: &mut Value,
method: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
) -> Result<MethodOutcome, EvalError> {
let Value::Deque { items, maxlen } = obj else {
return Err(type_mismatch("deque"));
};
methods::deque::dispatch_deque_method(items, maxlen.as_ref(), method, args, kwargs)
}
fn defaultdict_methods(
obj: &mut Value,
method: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
) -> Result<MethodOutcome, EvalError> {
let Value::DefaultDict(data) = obj else {
return Err(type_mismatch("defaultdict"));
};
methods::dict::dispatch_dict_method(&mut data.items, method, args, kwargs)
}
fn template_methods(
obj: &mut Value,
method: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
) -> Result<MethodOutcome, EvalError> {
let Value::Template(template) = obj else {
return Err(type_mismatch("Template"));
};
match method {
"substitute" | "safe_substitute" => {
let safe = method == "safe_substitute";
let rendered =
super::super::strings::template_substitute(template, args, kwargs, safe)?;
Ok(MethodOutcome::pure(Value::String(rendered.into())))
}
_ => Err(InterpreterError::AttributeError(format!(
"'string.Template' object has no attribute '{method}'"
))
.into()),
}
}
fn chainmap_methods(
obj: &mut Value,
method: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
) -> Result<MethodOutcome, EvalError> {
let Value::ChainMap(maps) = obj else {
return Err(type_mismatch("ChainMap"));
};
match method {
"new_child" => {
let child = match args.first() {
Some(v @ Value::Dict(_)) => v.clone(),
None | Some(Value::None) => Value::Dict(crate::value::shared_dict(IndexMap::new())),
Some(other) => {
return Err(InterpreterError::TypeError(format!(
"ChainMap.new_child() argument must be a mapping, not '{}'",
other.type_name()
))
.into());
}
};
let mut new_maps = Vec::with_capacity(maps.len() + 1);
new_maps.push(child);
new_maps.extend(maps.iter().cloned());
Ok(MethodOutcome::pure(Value::ChainMap(new_maps)))
}
"copy" => {
let mut new_maps = maps.clone();
let copied = match new_maps.first() {
Some(Value::Dict(first)) => Some(crate::value::shared_dict(first.lock().clone())),
_ => None,
};
if let Some(c) = copied {
new_maps[0] = Value::Dict(c);
}
Ok(MethodOutcome::pure(Value::ChainMap(new_maps)))
}
"keys" | "values" | "items" | "get" | "__contains__" => {
let mut merged = crate::types::chainmap_contents(maps);
methods::dict::dispatch_dict_method(&mut merged, method, args, kwargs)
}
_ => {
if let Some(Value::Dict(first)) = maps.first() {
let mut guard = first.lock();
methods::dict::dispatch_dict_method(&mut guard, method, args, kwargs)
} else {
Err(InterpreterError::AttributeError(format!(
"'ChainMap' object has no attribute '{method}'"
))
.into())
}
}
}
}
fn set_methods(
obj: &mut Value,
method: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
) -> Result<MethodOutcome, EvalError> {
let Value::Set(body) = obj else {
return Err(type_mismatch("set"));
};
methods::set::dispatch_set_method(body, method, args, kwargs)
}
fn frozenset_methods(
obj: &mut Value,
method: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
) -> Result<MethodOutcome, EvalError> {
let Value::Frozenset(body) = obj else {
return Err(type_mismatch("frozenset"));
};
methods::set::dispatch_frozenset_method(body, method, args, kwargs)
}
fn tuple_methods(
obj: &mut Value,
method: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
) -> Result<MethodOutcome, EvalError> {
let Value::Tuple(items) = obj else {
return Err(type_mismatch("tuple"));
};
methods::tuple::dispatch_tuple_method(items, method, args, kwargs).map(MethodOutcome::pure)
}
fn int_methods(
obj: &mut Value,
method: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
) -> Result<MethodOutcome, EvalError> {
if method == "to_bytes" {
let value = match obj {
Value::Int(i) => num_bigint::BigInt::from(*i),
Value::BigInt(b) => (**b).clone(),
Value::Bool(b) => num_bigint::BigInt::from(i64::from(*b)),
_ => return Err(type_mismatch("int")),
};
return super::helpers::int_to_bytes(&value, args, kwargs).map(MethodOutcome::pure);
}
match obj {
Value::Int(i) => {
methods::int::dispatch_int_method(*i, method, args, kwargs).map(MethodOutcome::pure)
}
Value::Bool(b) => methods::int::dispatch_int_method(i64::from(*b), method, args, kwargs)
.map(MethodOutcome::pure),
Value::BigInt(i) => match i64::try_from(i.as_ref()) {
Ok(n) => {
methods::int::dispatch_int_method(n, method, args, kwargs).map(MethodOutcome::pure)
}
Err(_) => methods::int::dispatch_bigint_method(i, method, args, kwargs)
.map(MethodOutcome::pure),
},
_ => Err(type_mismatch("int")),
}
}
fn float_methods(
obj: &mut Value,
method: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
) -> Result<MethodOutcome, EvalError> {
let Value::Float(f) = obj else { return Err(type_mismatch("float")) };
methods::float::dispatch_float_method(*f, method, args, kwargs).map(MethodOutcome::pure)
}
fn complex_methods(
obj: &mut Value,
method: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
) -> Result<MethodOutcome, EvalError> {
let Value::Complex(c) = obj else { return Err(type_mismatch("complex")) };
reject_kwargs(method, kwargs)?;
let binop = match method {
"__add__" => Some(crate::types::BinOp::Add),
"__sub__" => Some(crate::types::BinOp::Sub),
"__mul__" => Some(crate::types::BinOp::Mul),
"__truediv__" => Some(crate::types::BinOp::Div),
"__pow__" => Some(crate::types::BinOp::Pow),
_ => None,
};
if let Some(op) = binop {
let rhs = args.first().ok_or_else(|| {
EvalError::from(InterpreterError::TypeError(format!(
"{method}() takes exactly one argument (0 given)"
)))
})?;
return crate::eval::operations::apply_binop_builtin(op, obj, rhs).map(MethodOutcome::pure);
}
if !args.is_empty() {
return Err(InterpreterError::TypeError(format!("{method}() takes no arguments")).into());
}
match method {
"conjugate" => Ok(MethodOutcome::pure(Value::Complex(Box::new(c.conj())))),
"real" => Ok(MethodOutcome::pure(Value::Float(c.re))),
"imag" => Ok(MethodOutcome::pure(Value::Float(c.im))),
"__abs__" => Ok(MethodOutcome::pure(Value::Float(c.norm()))),
"__neg__" => Ok(MethodOutcome::pure(Value::Complex(Box::new(-**c)))),
"__pos__" | "__complex__" => Ok(MethodOutcome::pure(Value::Complex(c.clone()))),
"__bool__" => Ok(MethodOutcome::pure(Value::Bool(c.re != 0.0 || c.im != 0.0))),
_ => Err(InterpreterError::AttributeError(format!(
"'complex' object has no attribute '{method}'"
))
.into()),
}
}
fn bytes_methods(
obj: &mut Value,
method: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
) -> Result<MethodOutcome, EvalError> {
let Value::Bytes(b) = obj else {
return Err(type_mismatch("bytes"));
};
methods::bytes::dispatch_bytes_method(b, method, args, kwargs).map(MethodOutcome::pure)
}
fn bytearray_methods(
obj: &mut Value,
method: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
) -> Result<MethodOutcome, EvalError> {
let Value::ByteArray(b) = obj else {
return Err(type_mismatch("bytearray"));
};
methods::bytes::dispatch_bytearray_method(b, method, args, kwargs)
}
fn stringio_methods(
obj: &mut Value,
method: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
) -> Result<MethodOutcome, EvalError> {
let Value::StringIO(io) = obj else {
return Err(type_mismatch("StringIO"));
};
let stream = io.clone();
methods::stringio::dispatch_stringio_method(&stream, method, args, kwargs)
}
fn memoryview_methods(
obj: &mut Value,
method: &str,
_args: &[Value],
kwargs: &IndexMap<String, Value>,
) -> Result<MethodOutcome, EvalError> {
let Value::MemoryView(_) = obj else {
return Err(type_mismatch("memoryview"));
};
crate::eval::functions::reject_kwargs(method, kwargs)?;
let raw = crate::types::memoryview_bytes(obj);
methods::bytes::dispatch_memoryview_method(&raw, method).map(MethodOutcome::pure)
}
fn date_methods(
obj: &mut Value,
method: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
) -> Result<MethodOutcome, EvalError> {
let Value::Date(date) = obj else {
return Err(type_mismatch("date"));
};
crate::eval::modules::datetime::dispatch_date_method(*date, method, args, kwargs)
.map(MethodOutcome::pure)
}
fn datetime_methods(
obj: &mut Value,
method: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
) -> Result<MethodOutcome, EvalError> {
let Value::DateTime { dt, tz_offset_secs } = obj else {
return Err(type_mismatch("datetime"));
};
crate::eval::modules::datetime::dispatch_datetime_method(
*dt,
*tz_offset_secs,
method,
args,
kwargs,
)
.map(MethodOutcome::pure)
}
fn time_methods(
obj: &mut Value,
method: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
) -> Result<MethodOutcome, EvalError> {
let Value::Time(t) = obj else {
return Err(type_mismatch("time"));
};
crate::eval::modules::datetime::dispatch_time_method(*t, method, args, kwargs)
.map(MethodOutcome::pure)
}
fn timedelta_methods(
obj: &mut Value,
method: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
) -> Result<MethodOutcome, EvalError> {
let Value::TimeDelta(micros) = obj else {
return Err(type_mismatch("timedelta"));
};
crate::eval::modules::datetime::dispatch_timedelta_method(*micros, method, args, kwargs)
.map(MethodOutcome::pure)
}
fn slice_component(v: &Value) -> Result<Option<i64>, EvalError> {
match v {
Value::None => Ok(None),
Value::Int(n) => Ok(Some(*n)),
Value::Bool(b) => Ok(Some(i64::from(*b))),
Value::BigInt(b) => {
use num_traits::{Signed as _, ToPrimitive as _};
Ok(Some(b.to_i64().unwrap_or(if b.is_negative() { i64::MIN } else { i64::MAX })))
}
_ => Err(InterpreterError::TypeError(
"slice indices must be integers or None or have an __index__ method".into(),
)
.into()),
}
}
fn slice_methods(
obj: &mut Value,
method: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
) -> Result<MethodOutcome, EvalError> {
let Value::Slice(slice) = obj else {
return Err(type_mismatch("slice"));
};
crate::eval::functions::reject_kwargs(method, kwargs)?;
match method {
"indices" => {
let length = arg1(method, args)?.as_int().ok_or_else(|| {
EvalError::from(InterpreterError::TypeError(
"slice indices must be integers".into(),
))
})?;
if length < 0 {
return Err(
InterpreterError::ValueError("length should not be negative".into()).into()
);
}
let step = slice_component(&slice.step)?.unwrap_or(1);
if step == 0 {
return Err(InterpreterError::ValueError("slice step cannot be zero".into()).into());
}
let negative = step < 0;
let (lower, upper) = if negative { (-1, length - 1) } else { (0, length) };
let clamp = |raw: Option<i64>, default: i64| -> i64 {
match raw {
None => default,
Some(mut v) => {
if v < 0 {
v += length;
v.max(lower)
} else {
v.min(upper)
}
}
}
};
let start = clamp(slice_component(&slice.start)?, if negative { upper } else { lower });
let stop = clamp(slice_component(&slice.stop)?, if negative { lower } else { upper });
Ok(MethodOutcome::pure(Value::Tuple(vec![
Value::Int(start),
Value::Int(stop),
Value::Int(step),
])))
}
_ => Err(InterpreterError::AttributeError(format!(
"'slice' object has no attribute '{method}'"
))
.into()),
}
}
fn re_match_methods(
obj: &mut Value,
method: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
) -> Result<MethodOutcome, EvalError> {
let Value::ReMatch(m) = obj else {
return Err(type_mismatch("re.Match"));
};
crate::eval::modules::re::dispatch_match_method(m, method, args, kwargs)
.map(MethodOutcome::pure)
}
fn re_pattern_methods(
obj: &mut Value,
method: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
) -> Result<MethodOutcome, EvalError> {
let Value::RePattern(p) = obj else {
return Err(type_mismatch("re.Pattern"));
};
crate::eval::modules::re::dispatch_pattern_method(p, method, args, kwargs)
.map(MethodOutcome::pure)
}
fn fraction_methods(
obj: &mut Value,
method: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
) -> Result<MethodOutcome, EvalError> {
let Value::Fraction(f) = obj else {
return Err(type_mismatch("Fraction"));
};
crate::eval::functions::reject_kwargs(method, kwargs)?;
match method {
"limit_denominator" => {
let max_denom = match args.first() {
None => num_bigint::BigInt::from(1_000_000),
Some(v) => crate::value::value_as_bigint(v).ok_or_else(|| {
EvalError::from(InterpreterError::TypeError(
"limit_denominator() argument must be an integer".into(),
))
})?,
};
Ok(MethodOutcome::pure(Value::Fraction(Box::new(limit_denominator(f, &max_denom)))))
}
"as_integer_ratio" => Ok(MethodOutcome::pure(Value::Tuple(vec![
crate::value::int_from_bigint(f.numer().clone()),
crate::value::int_from_bigint(f.denom().clone()),
]))),
"__floor__" | "__ceil__" | "__trunc__" => {
let r = match method {
"__floor__" => f.floor(),
"__ceil__" => f.ceil(),
_ => f.trunc(),
};
Ok(MethodOutcome::pure(crate::value::int_from_bigint(r.to_integer())))
}
_ => Err(InterpreterError::AttributeError(format!(
"'Fraction' object has no attribute '{method}'"
))
.into()),
}
}
fn limit_denominator(
f: &num_rational::BigRational,
max_denominator: &num_bigint::BigInt,
) -> num_rational::BigRational {
use num_bigint::BigInt;
use num_rational::BigRational;
use num_traits::{One as _, Signed as _, Zero as _};
if max_denominator < &BigInt::one() {
return f.clone();
}
if f.denom() <= max_denominator {
return f.clone();
}
let (mut p0, mut q0, mut p1, mut q1) =
(BigInt::zero(), BigInt::one(), BigInt::one(), BigInt::zero());
let (mut n, mut d) = (f.numer().clone(), f.denom().clone());
loop {
let a = &n / &d;
let q2 = &q0 + &a * &q1;
if &q2 > max_denominator {
break;
}
let new_p1 = &p0 + &a * &p1;
p0 = std::mem::replace(&mut p1, new_p1);
q0 = std::mem::replace(&mut q1, q2);
let new_d = &n - &a * &d;
n = std::mem::replace(&mut d, new_d);
}
let k = (max_denominator - &q0) / &q1;
let bound1 = BigRational::new(&p0 + &k * &p1, &q0 + &k * &q1);
let bound2 = BigRational::new(p1, q1);
if (&bound2 - f).abs() <= (&bound1 - f).abs() { bound2 } else { bound1 }
}
fn decimal_methods(
obj: &mut Value,
method: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
) -> Result<MethodOutcome, EvalError> {
let Value::Decimal(d, kind) = obj else {
return Err(type_mismatch("Decimal"));
};
if !matches!(method, "quantize" | "to_integral_value" | "to_integral" | "to_integral_exact") {
crate::eval::functions::reject_kwargs(method, kwargs)?;
}
crate::eval::modules::decimal::dispatch_decimal_method(d, *kind, method, args, kwargs)
.map(MethodOutcome::pure)
}
fn hash_digest_methods(
obj: &mut Value,
method: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
) -> Result<MethodOutcome, EvalError> {
let Value::HashDigest { bytes, .. } = obj else {
return Err(type_mismatch("HASH"));
};
if method == "update" {
crate::eval::functions::reject_kwargs(method, kwargs)?;
let data = match args.first() {
Some(Value::Bytes(b)) => b.clone(),
Some(Value::ByteArray(b)) => b.lock().clone(),
_ => {
return Err(InterpreterError::TypeError(
"update() argument must be a bytes-like object".into(),
)
.into());
}
};
let grew = data.len();
bytes.extend_from_slice(&data);
return Ok(MethodOutcome::grew(Value::None, grew));
}
let Value::HashDigest { algo, bytes } = obj else {
return Err(type_mismatch("HASH"));
};
crate::eval::modules::hashlib::dispatch_hash_method(algo, bytes, method, args, kwargs)
.map(MethodOutcome::pure)
}
fn single_dispatch_methods(
obj: &mut Value,
method: &str,
args: &[Value],
_kwargs: &IndexMap<String, Value>,
) -> Result<MethodOutcome, EvalError> {
let dispatcher = obj.clone();
let Value::SingleDispatch(sd) = obj else {
return Err(type_mismatch("singledispatch"));
};
match method {
"register" => {
let subject = args.first().cloned().ok_or_else(|| {
EvalError::from(InterpreterError::TypeError(
"register() missing required argument".into(),
))
})?;
if let Some(type_name) = crate::eval::modules::functools::dispatch_type_name(&subject) {
return Ok(MethodOutcome::pure(Value::Partial(Box::new(
crate::value::PartialData {
func: Value::ModuleFunction {
module: "functools".into(),
name: "_sd_register_typed".into(),
},
args: vec![dispatcher, Value::String(type_name.into())],
keywords: IndexMap::new(),
},
))));
}
let type_name = crate::eval::modules::functools::first_param_annotation(&subject)
.ok_or_else(|| {
EvalError::from(InterpreterError::TypeError(
"Invalid first argument to `register()`: it must be a type or a callable \
with a type-annotated first argument"
.into(),
))
})?;
sd.registry.lock().insert(type_name, subject.clone());
Ok(MethodOutcome::pure(subject))
}
_ => Err(InterpreterError::AttributeError(format!(
"'function' object has no attribute '{method}'"
))
.into()),
}
}
fn type_mismatch(expected: &str) -> EvalError {
InterpreterError::TypeError(format!("internal: method table expected {expected}")).into()
}
fn methods_handler_for(obj: &Value) -> Option<MethodsHandler> {
match obj {
Value::String(_) => Some(str_methods),
Value::List(_) => Some(list_methods),
Value::Array { .. } => Some(array_methods),
Value::LruCache(_) => Some(lru_methods),
Value::SingleDispatch(_) => Some(single_dispatch_methods),
Value::Dict(_) | Value::OrderedDict(_) => Some(dict_methods),
Value::StringIO(_) => Some(stringio_methods),
Value::Counter(_) => Some(counter_methods),
Value::Deque { .. } => Some(deque_methods),
Value::DefaultDict(_) => Some(defaultdict_methods),
Value::ChainMap(_) => Some(chainmap_methods),
Value::Template(_) => Some(template_methods),
Value::Set(_) => Some(set_methods),
Value::Frozenset(_) => Some(frozenset_methods),
Value::Tuple(_) => Some(tuple_methods),
Value::Slice(_) => Some(slice_methods),
Value::Range { .. } => Some(range_methods),
Value::Int(_) | Value::BigInt(_) | Value::Bool(_) => Some(int_methods),
Value::Float(_) => Some(float_methods),
Value::Complex(_) => Some(complex_methods),
Value::Bytes(_) => Some(bytes_methods),
Value::ByteArray(_) => Some(bytearray_methods),
Value::MemoryView(_) => Some(memoryview_methods),
Value::Date(_) => Some(date_methods),
Value::DateTime { .. } => Some(datetime_methods),
Value::Time(_) => Some(time_methods),
Value::TimeDelta(_) => Some(timedelta_methods),
Value::ReMatch(_) => Some(re_match_methods),
Value::RePattern(_) => Some(re_pattern_methods),
Value::Decimal(..) => Some(decimal_methods),
Value::Fraction(_) => Some(fraction_methods),
Value::HashDigest { .. } => Some(hash_digest_methods),
_ => None,
}
}
fn try_builtin_dunder(
obj: &Value,
method: &str,
args: &[Value],
) -> Result<Option<MethodOutcome>, EvalError> {
let pure = |v: Value| Ok(Some(MethodOutcome::pure(v)));
match method {
"__len__" => match crate::types::dispatch_len(obj) {
Ok(n) => pure(Value::Int(crate::eval::functions::to_len_i64(n)?)),
Err(_) => Ok(None),
},
"__contains__" => match crate::types::dispatch_contains(obj, arg1(method, args)?) {
Ok(b) => pure(Value::Bool(b)),
Err(_) => Ok(None),
},
"__getitem__" => match crate::types::dispatch_getitem(obj, arg1(method, args)?) {
Ok(v) => pure(v),
Err(e) => Err(e),
},
"__str__" => pure(Value::String(format!("{obj}").into())),
"__repr__" => pure(Value::String(obj.repr().into())),
"__format__" => {
let spec = match args.first() {
Some(Value::String(s)) => s.as_str(),
None => "",
Some(other) => {
return Err(InterpreterError::TypeError(format!(
"__format__() argument 1 must be str, not {}",
other.type_name()
))
.into());
}
};
if spec.is_empty() {
pure(Value::String(format!("{obj}").into()))
} else {
crate::eval::strings::apply_format_spec(obj, spec)
.map(|v| Some(MethodOutcome::pure(v)))
}
}
"__bool__" => pure(Value::Bool(obj.is_truthy())),
"__floor__" | "__ceil__" | "__trunc__" => match numeric_integral(obj, method) {
Some(v) => pure(v),
None => Ok(None),
},
"__int__" => match numeric_integral(obj, "__trunc__") {
Some(v) => pure(v),
None => Ok(None),
},
"__index__" => match obj {
Value::Int(_) | Value::BigInt(_) => pure(obj.clone()),
Value::Bool(b) => pure(Value::Int(i64::from(*b))),
_ => Ok(None),
},
"__float__" => match obj {
Value::Int(_)
| Value::BigInt(_)
| Value::Float(_)
| Value::Bool(_)
| Value::Decimal(..)
| Value::Fraction(_) => match obj.as_float() {
Some(f) => pure(Value::Float(f)),
None => Ok(None),
},
_ => Ok(None),
},
"__round__" => {
use crate::eval::functions::{
round_bigint, round_decimal, round_float, round_fraction, round_int, value_to_i64,
};
let ndigits = match args.first() {
Some(v) => Some(value_to_i64(v)?),
None => None,
};
match obj {
Value::Int(i) => pure(round_int(*i, ndigits)),
Value::BigInt(b) => pure(crate::value::int_from_bigint(round_bigint(b, ndigits))),
Value::Bool(b) => pure(round_int(i64::from(*b), ndigits)),
Value::Float(f) => pure(round_float(*f, ndigits)?),
Value::Decimal(d, _) => pure(round_decimal(d, ndigits)),
Value::Fraction(fr) => pure(round_fraction(fr, ndigits)),
_ => Ok(None),
}
}
"__hash__" if !matches!(obj, Value::Instance(_) | Value::Class(_)) => {
match crate::pyhash::python_hash(obj) {
Some(h) => pure(Value::Int(h)),
None => Ok(None),
}
}
"__eq__" | "__ne__" | "__lt__" | "__le__" | "__gt__" | "__ge__"
if !matches!(obj, Value::Instance(_) | Value::Class(_)) =>
{
let Some(other) = args.first() else {
return Ok(None);
};
use crate::eval::operations::{compare_lt, values_equal_pub};
let eq_bool = |equal: bool| if eq_yields_bool(obj, other) { Some(equal) } else { None };
let outcome: Option<bool> = match method {
"__eq__" => eq_bool(values_equal_pub(obj, other)),
"__ne__" => eq_bool(!values_equal_pub(obj, other)),
"__lt__" => compare_lt(obj, other).ok(),
"__le__" => {
compare_lt(obj, other).ok().map(|lt| lt || values_equal_pub(obj, other))
}
"__gt__" => compare_lt(other, obj).ok(),
"__ge__" => {
compare_lt(other, obj).ok().map(|gt| gt || values_equal_pub(obj, other))
}
_ => None,
};
match outcome {
Some(b) => pure(Value::Bool(b)),
None => pure(Value::NotImplemented),
}
}
_ if !matches!(obj, Value::Instance(_) | Value::Class(_)) => {
let Some((op, reflected)) = arith_dunder_op(method) else {
return Ok(None);
};
let Some(other) = args.first() else {
return Ok(None);
};
let (lhs, rhs) = if reflected { (other, obj) } else { (obj, other) };
if method == "__divmod__" || method == "__rdivmod__" {
let q = crate::eval::operations::apply_binop(
lhs,
rhs,
rustpython_parser::ast::Operator::FloorDiv,
28,
1_048_576,
)?;
let r = crate::eval::operations::apply_binop(
lhs,
rhs,
rustpython_parser::ast::Operator::Mod,
28,
1_048_576,
)?;
return pure(Value::Tuple(vec![q, r]));
}
match crate::eval::operations::apply_binop(lhs, rhs, op, 28, 1_048_576) {
Ok(v) => pure(v),
Err(EvalError::Interpreter(InterpreterError::TypeError(_)))
if is_numeric_value(obj) =>
{
pure(Value::NotImplemented)
}
Err(e) => Err(e),
}
}
_ => Ok(None),
}
}
fn eq_yields_bool(lhs: &Value, rhs: &Value) -> bool {
use crate::value::EnumKind;
let int_like = |v: &Value| {
matches!(
v,
Value::Int(_)
| Value::BigInt(_)
| Value::Bool(_)
| Value::EnumMember { kind: EnumKind::Int | EnumKind::IntFlag, .. }
)
};
let same_enum = |rhs: &Value, class: &str| matches!(rhs, Value::EnumMember { class_name, .. } if class_name == class);
match lhs {
Value::Int(_) | Value::BigInt(_) | Value::Bool(_) => int_like(rhs),
Value::Float(_) => int_like(rhs) || matches!(rhs, Value::Float(_)),
Value::Complex(_) => int_like(rhs) || matches!(rhs, Value::Float(_) | Value::Complex(_)),
Value::Decimal(..) => {
int_like(rhs)
|| matches!(rhs, Value::Float(_) | Value::Decimal(..) | Value::Fraction(_))
}
Value::Fraction(_) => int_like(rhs) || matches!(rhs, Value::Float(_) | Value::Fraction(_)),
Value::String(_) => matches!(rhs, Value::String(_)),
Value::Bytes(_) => matches!(rhs, Value::Bytes(_)),
Value::ByteArray(_) => matches!(rhs, Value::Bytes(_) | Value::ByteArray(_)),
Value::List(_) => matches!(rhs, Value::List(_)),
Value::Tuple(_) => matches!(rhs, Value::Tuple(_)),
Value::Dict(_) | Value::OrderedDict(_) | Value::Counter(_) => {
matches!(rhs, Value::Dict(_) | Value::OrderedDict(_) | Value::Counter(_))
}
Value::Set(_) | Value::Frozenset(_) => matches!(rhs, Value::Set(_) | Value::Frozenset(_)),
Value::None => matches!(rhs, Value::None),
Value::Range { .. } => matches!(rhs, Value::Range { .. }),
Value::Date(_) | Value::DateTime { .. } => {
matches!(rhs, Value::Date(_) | Value::DateTime { .. })
}
Value::Time(_) => matches!(rhs, Value::Time(_)),
Value::TimeDelta(_) => matches!(rhs, Value::TimeDelta(_)),
Value::TimeZone(_) => matches!(rhs, Value::TimeZone(_)),
Value::EnumMember { kind, class_name, .. } => match kind {
EnumKind::Int | EnumKind::IntFlag => int_like(rhs) || same_enum(rhs, class_name),
EnumKind::Str => matches!(rhs, Value::String(_)) || same_enum(rhs, class_name),
EnumKind::Plain | EnumKind::Flag => same_enum(rhs, class_name),
},
_ => true,
}
}
fn is_numeric_value(v: &Value) -> bool {
use crate::value::EnumKind;
matches!(
v,
Value::Int(_)
| Value::BigInt(_)
| Value::Bool(_)
| Value::Float(_)
| Value::Complex(_)
| Value::Decimal(..)
| Value::Fraction(_)
| Value::EnumMember { kind: EnumKind::Int | EnumKind::IntFlag, .. }
)
}
fn arith_dunder_op(method: &str) -> Option<(rustpython_parser::ast::Operator, bool)> {
use rustpython_parser::ast::Operator::{
Add, BitAnd, BitOr, BitXor, Div, FloorDiv, LShift, MatMult, Mod, Mult, Pow, RShift, Sub,
};
let (op, reflected) = match method {
"__add__" => (Add, false),
"__radd__" => (Add, true),
"__sub__" => (Sub, false),
"__rsub__" => (Sub, true),
"__mul__" => (Mult, false),
"__rmul__" => (Mult, true),
"__truediv__" => (Div, false),
"__rtruediv__" => (Div, true),
"__floordiv__" => (FloorDiv, false),
"__rfloordiv__" => (FloorDiv, true),
"__mod__" => (Mod, false),
"__rmod__" => (Mod, true),
"__pow__" => (Pow, false),
"__rpow__" => (Pow, true),
"__matmul__" => (MatMult, false),
"__rmatmul__" => (MatMult, true),
"__and__" => (BitAnd, false),
"__rand__" => (BitAnd, true),
"__or__" => (BitOr, false),
"__ror__" => (BitOr, true),
"__xor__" => (BitXor, false),
"__rxor__" => (BitXor, true),
"__lshift__" => (LShift, false),
"__rlshift__" => (LShift, true),
"__rshift__" => (RShift, false),
"__rrshift__" => (RShift, true),
"__divmod__" => (Add, false),
"__rdivmod__" => (Add, true),
_ => return None,
};
Some((op, reflected))
}
fn numeric_integral(obj: &Value, method: &str) -> Option<Value> {
use num_traits::ToPrimitive as _;
match obj {
Value::Int(_) | Value::BigInt(_) => Some(obj.clone()),
Value::Bool(b) => Some(Value::Int(i64::from(*b))),
Value::Float(f) => {
let r = match method {
"__floor__" => f.floor(),
"__ceil__" => f.ceil(),
_ => f.trunc(),
};
r.to_i64().map(Value::Int)
}
Value::Fraction(fr) => {
let r = match method {
"__floor__" => fr.floor(),
"__ceil__" => fr.ceil(),
_ => fr.trunc(),
};
Some(crate::value::int_from_bigint(r.to_integer()))
}
Value::Decimal(d, _) => {
use bigdecimal::BigDecimal;
let rounding = match method {
"__floor__" => bigdecimal::RoundingMode::Floor,
"__ceil__" => bigdecimal::RoundingMode::Ceiling,
_ => bigdecimal::RoundingMode::Down,
};
let int_dec: BigDecimal = d.with_scale_round(0, rounding);
let (bigint, _) = int_dec.as_bigint_and_exponent();
Some(crate::value::int_from_bigint(bigint))
}
_ => None,
}
}
pub(super) fn dispatch_method(
obj: &mut Value,
method: &str,
args: &[Value],
kwargs: &IndexMap<String, Value>,
) -> Result<MethodOutcome, EvalError> {
if method.starts_with("__") {
if let Some(outcome) = try_builtin_dunder(obj, method, args)? {
return Ok(outcome);
}
}
let Some(handler) = methods_handler_for(obj) else {
debug_assert!(
!crate::types::type_has_methods_table(obj),
"type {} claims has_methods_table but has no handler",
crate::types::type_name_of(obj)
);
return Err(InterpreterError::AttributeError(format!(
"'{}' object has no attribute '{method}'",
obj.type_name()
))
.into());
};
handler(obj, method, args, kwargs)
}
pub(crate) fn arg1<'a>(method: &str, args: &'a [Value]) -> Result<&'a Value, EvalError> {
args.first().ok_or_else(|| {
EvalError::from(InterpreterError::TypeError(format!("{method}() takes exactly 1 argument")))
})
}