use std::{borrow::Cow, fmt};
use num_bigint::BigInt;
use crate::{
args::ArgValues,
bytecode::VM,
defer_drop,
exception_private::{ExcType, RunError, RunResult, SimpleException},
heap::{DropWithHeap, Heap, HeapData, HeapId},
intern::{Interns, StaticStrings, StringId},
resource::ResourceTracker,
types::{
AttrCallResult, Bytes, Dict, FrozenSet, List, LongInt, MontyIter, Path, PyTrait, Range, Set, Slice, Str,
TimeZone, Tuple, bytes::bytes_fromhex, date, datetime, dict::dict_fromkeys, instance::class_name,
long_int::INT_MAX_STR_DIGITS, str::StringRepr, timedelta,
},
value::Value,
};
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Hash,
serde::Serialize,
serde::Deserialize,
strum::EnumString,
strum::IntoStaticStr,
)]
#[strum(serialize_all = "lowercase")]
#[expect(
clippy::enum_variant_names,
reason = "`Type` and `NoneType` mirror the Python type names"
)]
pub enum Type {
Ellipsis,
Type,
#[strum(serialize = "NoneType")]
NoneType,
Bool,
Int,
Float,
Range,
Slice,
Date,
#[strum(serialize = "datetime.datetime")]
DateTime,
TimeDelta,
TimeZone,
Str,
Bytes,
List,
Tuple,
NamedTuple,
Dict,
#[strum(serialize = "dict_keys")]
DictKeys,
#[strum(serialize = "dict_items")]
DictItems,
#[strum(serialize = "dict_values")]
DictValues,
Set,
FrozenSet,
Dataclass,
#[strum(disabled)]
Instance(HeapId),
#[strum(disabled)]
Exception(ExcType),
Function,
#[strum(serialize = "builtin_function_or_method")]
BuiltinFunction,
Cell,
Iterator,
Coroutine,
Module,
#[strum(serialize = "_io.TextIOWrapper")]
TextIOWrapper,
#[strum(serialize = "_io.BufferedReader")]
BufferedReader,
#[strum(serialize = "_io.BufferedWriter")]
BufferedWriter,
#[strum(serialize = "_io.BufferedRandom")]
BufferedRandom,
#[strum(serialize = "typing._SpecialForm")]
SpecialForm,
#[strum(serialize = "PosixPath")]
Path,
Property,
#[strum(serialize = "re.Pattern")]
RePattern,
#[strum(serialize = "re.Match")]
ReMatch,
}
impl fmt::Display for Type {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match *self {
Self::Exception(exc_type) => exc_type.into(),
Self::Instance(_) => unreachable!("Type::Instance must be rendered via Type::name"),
other => other.into(),
})
}
}
impl Type {
pub(crate) fn name<'i>(self, heap: &Heap<impl ResourceTracker>, interns: &'i Interns) -> Cow<'i, str> {
match self {
Self::Instance(class_id) => class_name(class_id, heap, interns),
Self::Exception(exc_type) => Cow::Borrowed(exc_type.into()),
other => Cow::Borrowed(other.into()),
}
}
pub(crate) fn cpython_arg_name<'i>(self, heap: &Heap<impl ResourceTracker>, interns: &'i Interns) -> Cow<'i, str> {
match self {
Self::NoneType => Cow::Borrowed("None"),
other => other.name(heap, interns),
}
}
#[must_use]
pub const fn builtin_name(self) -> Option<&'static str> {
match self {
Self::Bool => Some("bool"),
Self::Int => Some("int"),
Self::Float => Some("float"),
Self::Str => Some("str"),
Self::Bytes => Some("bytes"),
Self::List => Some("list"),
Self::Tuple => Some("tuple"),
Self::Dict => Some("dict"),
Self::Set => Some("set"),
Self::FrozenSet => Some("frozenset"),
Self::Range => Some("range"),
Self::Slice => Some("slice"),
Self::Iterator => Some("iter"),
Self::Type => Some("type"),
Self::Property => Some("property"),
_ => None,
}
}
#[must_use]
pub fn from_builtin_name(name: &str) -> Option<Self> {
match name {
"bool" => Some(Self::Bool),
"int" => Some(Self::Int),
"float" => Some(Self::Float),
"str" => Some(Self::Str),
"bytes" => Some(Self::Bytes),
"list" => Some(Self::List),
"tuple" => Some(Self::Tuple),
"dict" => Some(Self::Dict),
"set" => Some(Self::Set),
"frozenset" => Some(Self::FrozenSet),
"range" => Some(Self::Range),
"slice" => Some(Self::Slice),
"iter" => Some(Self::Iterator),
"type" => Some(Self::Type),
"property" => Some(Self::Property),
_ => None,
}
}
#[must_use]
pub(crate) fn from_type_name(name: &str) -> Option<Self> {
name.parse::<Self>()
.ok()
.or_else(|| name.parse::<ExcType>().ok().map(Self::Exception))
}
#[must_use]
pub fn is_instance_of(self, other: Self) -> bool {
if self == other {
true
} else if self == Self::Bool && other == Self::Int {
true
} else if self == Self::DateTime && other == Self::Date {
true
} else {
false
}
}
#[must_use]
pub fn callable_to_u8(self) -> Option<u8> {
match self {
Self::Bool => Some(0),
Self::Int => Some(1),
Self::Float => Some(2),
Self::Str => Some(3),
Self::Bytes => Some(4),
Self::List => Some(5),
Self::Tuple => Some(6),
Self::Dict => Some(7),
Self::Set => Some(8),
Self::FrozenSet => Some(9),
Self::Range => Some(10),
Self::Slice => Some(11),
Self::Iterator => Some(12),
Self::Path => Some(13),
_ => None,
}
}
#[must_use]
pub fn callable_from_u8(id: u8) -> Option<Self> {
match id {
0 => Some(Self::Bool),
1 => Some(Self::Int),
2 => Some(Self::Float),
3 => Some(Self::Str),
4 => Some(Self::Bytes),
5 => Some(Self::List),
6 => Some(Self::Tuple),
7 => Some(Self::Dict),
8 => Some(Self::Set),
9 => Some(Self::FrozenSet),
10 => Some(Self::Range),
11 => Some(Self::Slice),
12 => Some(Self::Iterator),
13 => Some(Self::Path),
_ => None,
}
}
pub(crate) fn call_class_method(
self,
method_id: StringId,
args: ArgValues,
vm: &mut VM<'_, impl ResourceTracker>,
) -> RunResult<AttrCallResult> {
match (self, method_id) {
(Self::Dict, m) if m == StaticStrings::Fromkeys => dict_fromkeys(args, vm).map(AttrCallResult::Value),
(Self::Bytes, m) if m == StaticStrings::Fromhex => bytes_fromhex(args, vm).map(AttrCallResult::Value),
(Self::Date, m) if m == StaticStrings::Today => date::class_today(vm.heap, args),
(Self::Date, m) if m == StaticStrings::Fromisoformat => {
date::class_fromisoformat(vm.heap, args, vm.interns).map(AttrCallResult::Value)
}
(Self::DateTime, m) if m == StaticStrings::Now => datetime::class_now(vm, args),
(Self::DateTime, m) if m == StaticStrings::Strptime => {
datetime::class_strptime(vm.heap, args, vm.interns).map(AttrCallResult::Value)
}
(Self::DateTime, m) if m == StaticStrings::Fromisoformat => {
datetime::class_fromisoformat(vm.heap, args, vm.interns).map(AttrCallResult::Value)
}
_ => {
let method_name = vm.interns.get_str(method_id);
args.drop_with_heap(vm.heap);
Err(ExcType::attribute_error(self, method_name))
}
}
}
pub(crate) fn call(self, vm: &mut VM<'_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {
match self {
Self::List => List::init(vm, args),
Self::Tuple => Tuple::init(vm, args),
Self::Dict => Dict::init(vm, args),
Self::Set => Set::init(vm, args),
Self::FrozenSet => FrozenSet::init(vm, args),
Self::Str => Str::init(vm, args),
Self::Bytes => Bytes::init(vm, args),
Self::Range => Range::init(vm, args),
Self::Slice => Slice::init(vm, args),
Self::Date => date::init(vm, args),
Self::DateTime => datetime::init(vm, args),
Self::TimeDelta => timedelta::init(vm, args),
Self::TimeZone => TimeZone::init(vm, args),
Self::Iterator => MontyIter::init(vm, args),
Self::Path => Path::init(vm, args),
Self::Int => {
let interns = vm.interns;
let Some(v) = args.get_zero_one_arg("int", vm.heap)? else {
return Ok(Value::Int(0));
};
defer_drop!(v, vm);
match v {
Value::Int(i) => Ok(Value::Int(*i)),
Value::Float(f) => Ok(Value::Int(f64_to_i64_truncate(*f))),
Value::Bool(b) => Ok(Value::Int(i64::from(*b))),
Value::InternString(string_id) => parse_int_from_str(interns.get_str(*string_id), vm.heap),
Value::Ref(heap_id) => match vm.heap.get(*heap_id) {
HeapData::Str(s) => parse_int_from_str(s.as_str(), vm.heap),
HeapData::LongInt(_) => Ok(v.clone_with_heap(vm.heap)),
_ => Err(ExcType::type_error_int_conversion(&v.py_type_name(vm))),
},
_ => Err(ExcType::type_error_int_conversion(&v.py_type_name(vm))),
}
}
Self::Float => {
let interns = vm.interns;
let Some(v) = args.get_zero_one_arg("float", vm.heap)? else {
return Ok(Value::Float(0.0));
};
defer_drop!(v, vm);
match v {
Value::Float(f) => Ok(Value::Float(*f)),
Value::Int(i) => Ok(Value::Float(*i as f64)),
Value::Bool(b) => Ok(Value::Float(if *b { 1.0 } else { 0.0 })),
Value::InternString(string_id) => {
Ok(Value::Float(parse_f64_from_str(interns.get_str(*string_id))?))
}
Value::Ref(heap_id) => match vm.heap.get(*heap_id) {
HeapData::Str(s) => Ok(Value::Float(parse_f64_from_str(s.as_str())?)),
_ => Err(ExcType::type_error_float_conversion(&v.py_type_name(vm))),
},
_ => Err(ExcType::type_error_float_conversion(&v.py_type_name(vm))),
}
}
Self::Bool => {
let Some(v) = args.get_zero_one_arg("bool", vm.heap)? else {
return Ok(Value::Bool(false));
};
defer_drop!(v, vm);
Ok(Value::Bool(v.py_bool(vm)))
}
_ => Err(ExcType::type_error_not_callable(&self.name(vm.heap, vm.interns))),
}
}
}
fn f64_to_i64_truncate(value: f64) -> i64 {
let truncated = value.trunc();
if truncated >= i64::MAX as f64 {
i64::MAX
} else if truncated <= i64::MIN as f64 {
i64::MIN
} else {
#[expect(clippy::cast_possible_truncation, reason = "bounds checked above")]
let result = truncated as i64;
result
}
}
fn parse_f64_from_str(value: &str) -> RunResult<f64> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(value_error_could_not_convert_string_to_float(value));
}
let lower = trimmed.to_ascii_lowercase();
let parsed = match lower.as_str() {
"inf" | "+inf" | "infinity" | "+infinity" => f64::INFINITY,
"-inf" | "-infinity" => f64::NEG_INFINITY,
"nan" | "+nan" => f64::NAN,
"-nan" => -f64::NAN,
_ => trimmed
.parse::<f64>()
.map_err(|_| value_error_could_not_convert_string_to_float(value))?,
};
Ok(parsed)
}
fn value_error_could_not_convert_string_to_float(value: &str) -> RunError {
SimpleException::new_msg(
ExcType::ValueError,
format!("could not convert string to float: {}", StringRepr(value)),
)
.into()
}
fn parse_int_from_str(value: &str, heap: &Heap<impl ResourceTracker>) -> RunResult<Value> {
let invalid = || ExcType::value_error_invalid_literal_for_int(StringRepr(value));
if let Ok(int) = value.parse::<i64>() {
return Ok(Value::Int(int));
}
let trimmed = value.trim();
if let Ok(int) = trimmed.parse::<i64>() {
return Ok(Value::Int(int));
}
if !is_valid_int_underscores(trimmed) {
return Err(invalid());
}
let normalized = trimmed.replace('_', "");
if let Ok(int) = normalized.parse::<i64>() {
Ok(Value::Int(int))
} else if normalized.len() > INT_MAX_STR_DIGITS {
let digit_count = normalized.bytes().filter(u8::is_ascii_digit).count();
let has_sign = normalized.starts_with(['+', '-']);
if digit_count + usize::from(has_sign) != normalized.len() || digit_count == 0 {
Err(invalid())
} else if digit_count > INT_MAX_STR_DIGITS {
Err(ExcType::value_error_int_str_too_large(digit_count))
} else {
let bi = normalized.parse::<BigInt>().map_err(|_| invalid())?;
Ok(LongInt::new(bi).into_value(heap)?)
}
} else if let Ok(bi) = normalized.parse::<BigInt>() {
Ok(LongInt::new(bi).into_value(heap)?)
} else {
Err(invalid())
}
}
fn is_valid_int_underscores(s: &str) -> bool {
if !s.contains('_') {
return true;
}
let digits = s.strip_prefix(['+', '-']).unwrap_or(s);
!digits.starts_with('_') && !digits.ends_with('_') && !digits.contains("__")
}