use std::{cell::Cell, fmt, fmt::Write, mem, ops};
use smallvec::smallvec;
use unicode_general_category::{GeneralCategory, get_general_category};
use super::{Bytes, CmpOrder, MontyIter, PyTrait};
use crate::{
args::{ArgValues, FromArgs, StrArg},
bytecode::{CallResult, VM},
codecs::Codec,
defer_drop, defer_drop_mut,
exception_private::{ExcType, RunResult},
hash::{HashValue, hash_python_str},
heap::{DropWithHeap, Heap, HeapData, HeapGuard, HeapId, HeapItem, HeapRead, heap_read_ref_as_field},
intern::{StaticStrings, StringId},
resource::{ResourceError, ResourceTracker, check_replace_size},
string_builder::StringBuilder,
types::{
LazyHeapSet, Type,
slice::{normalize_sequence_index, slice_collect_iterator},
},
value::{EitherStr, Value, eq_str},
};
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
pub(crate) struct Str(Box<str>, #[serde(skip)] Cell<Option<HashValue>>);
impl PartialEq for Str {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl Str {
#[must_use]
fn new(s: impl Into<Box<str>>) -> Self {
Self(s.into(), Cell::new(None))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
pub fn init(vm: &mut VM<'_, impl ResourceTracker>, args: ArgValues) -> RunResult<Value> {
let StrInitArgs { object } = StrInitArgs::from_args(args, vm)?;
match object {
None => Ok(Value::InternString(StaticStrings::EmptyString.into())),
Some(v) => {
defer_drop!(v, vm);
v.py_str(vm)
}
}
}
fn getitem_slice(&self, vm: &VM<'_, impl ResourceTracker>, slice: &super::Slice) -> RunResult<Value> {
let result_str: Box<str> = slice_collect_iterator(vm, slice, self.0.chars(), |c| c)?;
Ok(allocate_string(result_str, vm.heap)?)
}
}
#[derive(FromArgs)]
#[from_args(name = "str", style = c_named)]
struct StrInitArgs {
#[from_args(default)]
object: Option<Value>,
}
pub fn allocate_string(
s: impl AsRef<str> + Into<Box<str>>,
heap: &Heap<impl ResourceTracker>,
) -> Result<Value, ResourceError> {
let bytes = s.as_ref().as_bytes();
match bytes.len() {
0 => Ok(Value::InternString(StaticStrings::EmptyString.into())),
1 => Ok(Value::InternString(StringId::from_ascii(bytes[0]))),
_ => allocate_string_no_interning(s, heap),
}
}
pub fn allocate_string_no_interning(
s: impl Into<Box<str>>,
heap: &Heap<impl ResourceTracker>,
) -> Result<Value, ResourceError> {
let heap_id = heap.allocate(HeapData::Str(Str::new(s)))?;
Ok(Value::Ref(heap_id))
}
pub fn allocate_char(c: char, heap: &Heap<impl ResourceTracker>) -> Result<Value, ResourceError> {
if c.is_ascii() {
Ok(Value::InternString(StringId::from_ascii(c as u8)))
} else {
let heap_id = heap.allocate(HeapData::Str(Str::new(c.to_string())))?;
Ok(Value::Ref(heap_id))
}
}
pub(crate) fn concat_allocate_str(a: &str, b: &str, heap: &Heap<impl ResourceTracker>) -> Result<Value, ResourceError> {
let mut concat = String::with_capacity(a.len() + b.len());
concat.push_str(a);
concat.push_str(b);
allocate_string(concat, heap)
}
pub fn get_char_at_index(s: &str, index: i64) -> Option<char> {
let char_count = s.chars().count();
let len = i64::try_from(char_count).ok()?;
let normalized = if index < 0 { index + len } else { index };
if normalized < 0 || normalized >= len {
return None;
}
let idx = usize::try_from(normalized).ok()?;
s.chars().nth(idx)
}
impl ops::Deref for Str {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'h> PyTrait<'h> for HeapRead<'h, Str> {
fn py_type(&self, _vm: &VM<'h, impl ResourceTracker>) -> Type {
Type::Str
}
fn py_len(&self, vm: &VM<'h, impl ResourceTracker>) -> Option<usize> {
Some(self.get(vm.heap).0.chars().count())
}
fn py_getitem(&self, key: &Value, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<Value> {
if let Value::Ref(id) = key
&& let HeapData::Slice(slice) = vm.heap.get(*id)
{
return self.get(vm.heap).getitem_slice(vm, slice);
}
let index = key.as_index(vm, Type::Str)?;
let s = self.get(vm.heap);
let c = get_char_at_index(&s.0, index).ok_or_else(ExcType::str_index_error)?;
Ok(allocate_char(c, vm.heap)?)
}
fn py_eq_impl(&self, other: &Value, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<Option<bool>> {
Ok(eq_str(self.get(vm.heap).as_str(), other, vm))
}
fn py_hash(&self, _self_id: HeapId, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<Option<HashValue>> {
let s = self.get(vm.heap);
if let Some(cached) = s.1.get() {
return Ok(Some(cached));
}
let hash = hash_python_str(s.as_str());
s.1.set(Some(hash));
Ok(Some(hash))
}
fn py_bool(&self, vm: &mut VM<'h, impl ResourceTracker>) -> bool {
!self.get(vm.heap).0.is_empty()
}
fn py_cmp(&self, other: &Self, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<CmpOrder> {
Ok(CmpOrder::Ordered(self.get(vm.heap).0.cmp(&other.get(vm.heap).0)))
}
fn py_repr_fmt(
&self,
f: &mut impl Write,
vm: &mut VM<'h, impl ResourceTracker>,
_heap_ids: &mut LazyHeapSet,
) -> RunResult<()> {
Ok(string_repr_fmt(&self.get(vm.heap).0, f)?)
}
fn py_str(&self, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<Value> {
Ok(allocate_string(self.get(vm.heap).as_str(), vm.heap)?)
}
fn py_add(&self, other: &Self, vm: &mut VM<'h, impl ResourceTracker>) -> Result<Option<Value>, ResourceError> {
Ok(Some(concat_allocate_str(
self.get(vm.heap).as_str(),
other.get(vm.heap).as_str(),
vm.heap,
)?))
}
fn py_call_attr(
&mut self,
_self_id: HeapId,
vm: &mut VM<'h, impl ResourceTracker>,
attr: &EitherStr,
args: ArgValues,
) -> RunResult<CallResult> {
let Some(method) = attr.static_string() else {
args.drop_with_heap(vm);
return Err(ExcType::attribute_error(Type::Str, attr.as_str(vm.interns)));
};
let s = heap_read_ref_as_field!(self, Str, 0);
let s = s.as_box_value(vm.heap);
call_str_method_impl(&s, method, args, vm).map(CallResult::Value)
}
}
impl HeapItem for Str {
fn py_estimate_size(&self) -> usize {
mem::size_of::<Self>() + self.0.len()
}
fn py_dec_ref_ids(&mut self, _stack: &mut Vec<HeapId>) {
}
}
pub fn call_str_method(
s: &str,
method_id: StringId,
args: ArgValues,
vm: &mut VM<'_, impl ResourceTracker>,
) -> RunResult<Value> {
let args_guard = HeapGuard::new(args, vm.heap);
let Some(method) = StaticStrings::from_string_id(method_id) else {
return Err(ExcType::attribute_error(Type::Str, vm.interns.get_str(method_id)));
};
let args = args_guard.into_inner();
call_str_method_impl(&vm.heap.protect(s), method, args, vm)
}
fn call_str_method_impl<'h>(
s: &HeapRead<'h, str>,
method: StaticStrings,
args: ArgValues,
vm: &mut VM<'h, impl ResourceTracker>,
) -> RunResult<Value> {
match method {
StaticStrings::Lower => {
args.check_zero_args("str.lower", vm.heap)?;
str_lower(s.get(vm.heap), vm)
}
StaticStrings::Upper => {
args.check_zero_args("str.upper", vm.heap)?;
str_upper(s.get(vm.heap), vm)
}
StaticStrings::Capitalize => {
args.check_zero_args("str.capitalize", vm.heap)?;
str_capitalize(s.get(vm.heap), vm)
}
StaticStrings::Title => {
args.check_zero_args("str.title", vm.heap)?;
str_title(s.get(vm.heap), vm)
}
StaticStrings::Swapcase => {
args.check_zero_args("str.swapcase", vm.heap)?;
str_swapcase(s.get(vm.heap), vm)
}
StaticStrings::Casefold => {
args.check_zero_args("str.casefold", vm.heap)?;
str_casefold(s.get(vm.heap), vm)
}
StaticStrings::Isalpha => {
args.check_zero_args("str.isalpha", vm.heap)?;
Ok(Value::Bool(str_isalpha(s.get(vm.heap))))
}
StaticStrings::Isdigit => {
args.check_zero_args("str.isdigit", vm.heap)?;
Ok(Value::Bool(str_isdigit(s.get(vm.heap))))
}
StaticStrings::Isalnum => {
args.check_zero_args("str.isalnum", vm.heap)?;
Ok(Value::Bool(str_isalnum(s.get(vm.heap))))
}
StaticStrings::Isnumeric => {
args.check_zero_args("str.isnumeric", vm.heap)?;
Ok(Value::Bool(str_isnumeric(s.get(vm.heap))))
}
StaticStrings::Isspace => {
args.check_zero_args("str.isspace", vm.heap)?;
Ok(Value::Bool(str_isspace(s.get(vm.heap))))
}
StaticStrings::Islower => {
args.check_zero_args("str.islower", vm.heap)?;
Ok(Value::Bool(str_islower(s.get(vm.heap))))
}
StaticStrings::Isupper => {
args.check_zero_args("str.isupper", vm.heap)?;
Ok(Value::Bool(str_isupper(s.get(vm.heap))))
}
StaticStrings::Isascii => {
args.check_zero_args("str.isascii", vm.heap)?;
Ok(Value::Bool(s.get(vm.heap).is_ascii()))
}
StaticStrings::Isdecimal => {
args.check_zero_args("str.isdecimal", vm.heap)?;
Ok(Value::Bool(str_isdecimal(s.get(vm.heap))))
}
StaticStrings::Find => str_find(s, args, vm),
StaticStrings::Rfind => str_rfind(s, args, vm),
StaticStrings::Index => str_index(s, args, vm),
StaticStrings::Rindex => str_rindex(s, args, vm),
StaticStrings::Count => str_count(s, args, vm),
StaticStrings::Startswith => str_startswith(s, args, vm),
StaticStrings::Endswith => str_endswith(s, args, vm),
StaticStrings::Strip => str_strip(s, args, vm),
StaticStrings::Lstrip => str_lstrip(s, args, vm),
StaticStrings::Rstrip => str_rstrip(s, args, vm),
StaticStrings::Removeprefix => str_removeprefix(s, args, vm),
StaticStrings::Removesuffix => str_removesuffix(s, args, vm),
StaticStrings::Split => str_split(s, args, vm),
StaticStrings::Rsplit => str_rsplit(s, args, vm),
StaticStrings::Splitlines => str_splitlines(s, args, vm),
StaticStrings::Partition => str_partition(s, args, vm),
StaticStrings::Rpartition => str_rpartition(s, args, vm),
StaticStrings::Replace => str_replace(s, args, vm),
StaticStrings::Center => str_center(s, args, vm),
StaticStrings::Ljust => str_ljust(s, args, vm),
StaticStrings::Rjust => str_rjust(s, args, vm),
StaticStrings::Zfill => str_zfill(s, args, vm),
StaticStrings::Expandtabs => str_expandtabs(s, args, vm),
StaticStrings::Encode => str_encode(s, args, vm),
StaticStrings::Isidentifier => {
args.check_zero_args("str.isidentifier", vm.heap)?;
Ok(Value::Bool(str_isidentifier(s.get(vm.heap))))
}
StaticStrings::Istitle => {
args.check_zero_args("str.istitle", vm.heap)?;
Ok(Value::Bool(str_istitle(s.get(vm.heap))))
}
StaticStrings::Join => {
let iterable = args.get_one_arg("str.join", vm.heap)?;
str_join(s, iterable, vm)
}
_ => {
args.drop_with_heap(vm);
Err(ExcType::attribute_error(Type::Str, method.into()))
}
}
}
fn str_join<'h>(
separator: &HeapRead<'h, str>,
iterable: Value,
vm: &mut VM<'h, impl ResourceTracker>,
) -> RunResult<Value> {
let Ok(iter) = MontyIter::new(iterable, vm) else {
return Err(ExcType::type_error_join_not_iterable());
};
defer_drop_mut!(iter, vm);
let mut result = String::new();
let mut index = 0usize;
while let Some(item) = iter.for_next(vm)? {
defer_drop!(item, vm);
if index > 0 {
result.push_str(separator.get(vm.heap));
}
match item {
Value::InternString(id) => {
result.push_str(vm.interns.get_str(*id));
}
Value::Ref(heap_id) => {
if let HeapData::Str(s) = vm.heap.get(*heap_id) {
result.push_str(s.as_str());
} else {
let t = item.py_type_name(vm);
return Err(ExcType::type_error_join_item(index, &t));
}
}
_ => {
let t = item.py_type_name(vm);
return Err(ExcType::type_error_join_item(index, &t));
}
}
index += 1;
}
Ok(allocate_string(result, vm.heap)?)
}
pub fn string_repr_fmt(s: &str, f: &mut impl Write) -> fmt::Result {
let quote = if s.contains('\'') && !s.contains('"') {
'"'
} else {
'\''
};
f.write_char(quote)?;
for c in s.chars() {
match c {
'\\' => f.write_str("\\\\")?,
'\n' => f.write_str("\\n")?,
'\t' => f.write_str("\\t")?,
'\r' => f.write_str("\\r")?,
_ if c == quote => {
f.write_char('\\')?;
f.write_char(quote)?;
}
_ if repr_needs_escape(c) => write_char_escape(c, f)?,
_ => f.write_char(c)?,
}
}
f.write_char(quote)
}
fn repr_needs_escape(c: char) -> bool {
c != ' '
&& matches!(
get_general_category(c),
GeneralCategory::Control
| GeneralCategory::Format
| GeneralCategory::Surrogate
| GeneralCategory::PrivateUse
| GeneralCategory::Unassigned
| GeneralCategory::LineSeparator
| GeneralCategory::ParagraphSeparator
| GeneralCategory::SpaceSeparator
)
}
fn write_char_escape(c: char, f: &mut impl Write) -> fmt::Result {
let cp = c as u32;
if cp <= 0xFF {
write!(f, "\\x{cp:02x}")
} else if cp <= 0xFFFF {
write!(f, "\\u{cp:04x}")
} else {
write!(f, "\\U{cp:08x}")
}
}
#[derive(Debug)]
pub struct StringRepr<'a>(pub &'a str);
impl fmt::Display for StringRepr<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
string_repr_fmt(self.0, f)
}
}
fn str_lower(s: &str, vm: &VM<'_, impl ResourceTracker>) -> RunResult<Value> {
Ok(allocate_string(s.to_lowercase(), vm.heap)?)
}
fn str_upper(s: &str, vm: &VM<'_, impl ResourceTracker>) -> RunResult<Value> {
Ok(allocate_string(s.to_uppercase(), vm.heap)?)
}
fn str_capitalize(s: &str, vm: &VM<'_, impl ResourceTracker>) -> RunResult<Value> {
let mut chars = s.chars();
let result = match chars.next() {
None => String::new(),
Some(first) => {
let mut result = first.to_uppercase().to_string();
for c in chars {
result.extend(c.to_lowercase());
}
result
}
};
Ok(allocate_string(result, vm.heap)?)
}
fn str_title(s: &str, vm: &VM<'_, impl ResourceTracker>) -> RunResult<Value> {
let mut result = String::with_capacity(s.len());
let mut prev_is_cased = false;
for c in s.chars() {
if prev_is_cased {
result.extend(c.to_lowercase());
} else {
result.extend(c.to_uppercase());
}
prev_is_cased = c.is_alphabetic();
}
Ok(allocate_string(result, vm.heap)?)
}
fn str_swapcase(s: &str, vm: &VM<'_, impl ResourceTracker>) -> RunResult<Value> {
let mut result = String::with_capacity(s.len());
for c in s.chars() {
if c.is_uppercase() {
result.extend(c.to_lowercase());
} else if c.is_lowercase() {
result.extend(c.to_uppercase());
} else {
result.push(c);
}
}
Ok(allocate_string(result, vm.heap)?)
}
fn str_casefold(s: &str, vm: &VM<'_, impl ResourceTracker>) -> RunResult<Value> {
Ok(allocate_string(s.to_lowercase(), vm.heap)?)
}
fn str_isalpha(s: &str) -> bool {
!s.is_empty() && s.chars().all(char::is_alphabetic)
}
fn str_isdigit(s: &str) -> bool {
!s.is_empty() && s.chars().all(is_unicode_digit)
}
fn str_isalnum(s: &str) -> bool {
!s.is_empty() && s.chars().all(char::is_alphanumeric)
}
fn str_isnumeric(s: &str) -> bool {
!s.is_empty() && s.chars().all(char::is_numeric)
}
fn str_isspace(s: &str) -> bool {
!s.is_empty() && s.chars().all(char::is_whitespace)
}
fn str_islower(s: &str) -> bool {
let mut has_cased = false;
for c in s.chars() {
if c.is_uppercase() {
return false;
}
if c.is_lowercase() {
has_cased = true;
}
}
has_cased
}
fn str_isupper(s: &str) -> bool {
let mut has_cased = false;
for c in s.chars() {
if c.is_lowercase() {
return false;
}
if c.is_uppercase() {
has_cased = true;
}
}
has_cased
}
fn str_isdecimal(s: &str) -> bool {
!s.is_empty() && s.chars().all(is_unicode_decimal)
}
fn is_unicode_decimal(c: char) -> bool {
let cp = c as u32;
matches!(
cp,
0x0030..=0x0039
| 0x0660..=0x0669
| 0x06F0..=0x06F9
| 0x07C0..=0x07C9
| 0x0966..=0x096F
| 0x09E6..=0x09EF
| 0x0A66..=0x0A6F
| 0x0AE6..=0x0AEF
| 0x0B66..=0x0B6F
| 0x0BE6..=0x0BEF
| 0x0C66..=0x0C6F
| 0x0CE6..=0x0CEF
| 0x0D66..=0x0D6F
| 0x0DE6..=0x0DEF
| 0x0E50..=0x0E59
| 0x0ED0..=0x0ED9
| 0x0F20..=0x0F29
| 0x1040..=0x1049
| 0x1090..=0x1099
| 0x17E0..=0x17E9
| 0x1810..=0x1819
| 0x1946..=0x194F
| 0x19D0..=0x19D9
| 0x1A80..=0x1A89
| 0x1A90..=0x1A99
| 0x1B50..=0x1B59
| 0x1BB0..=0x1BB9
| 0x1C40..=0x1C49
| 0x1C50..=0x1C59
| 0xA620..=0xA629
| 0xA8D0..=0xA8D9
| 0xA900..=0xA909
| 0xA9D0..=0xA9D9
| 0xA9F0..=0xA9F9
| 0xAA50..=0xAA59
| 0xABF0..=0xABF9
| 0xFF10..=0xFF19
| 0x104A0..=0x104A9
| 0x10D30..=0x10D39
| 0x11066..=0x1106F
| 0x110F0..=0x110F9
| 0x11136..=0x1113F
| 0x111D0..=0x111D9
| 0x112F0..=0x112F9
| 0x11450..=0x11459
| 0x114D0..=0x114D9
| 0x11650..=0x11659
| 0x116C0..=0x116C9
| 0x11730..=0x11739
| 0x118E0..=0x118E9
| 0x11950..=0x11959
| 0x11C50..=0x11C59
| 0x11D50..=0x11D59
| 0x11DA0..=0x11DA9
| 0x1E950..=0x1E959
| 0x1FBF0..=0x1FBF9
)
}
fn is_unicode_digit(c: char) -> bool {
if is_unicode_decimal(c) {
return true;
}
let cp = c as u32;
matches!(
cp,
0x00B2..=0x00B3
| 0x00B9
| 0x2070
| 0x2074..=0x2079
| 0x2080..=0x2089
| 0x2460..=0x2468
| 0x24EA
| 0x2469..=0x2473
| 0x2474..=0x247C
| 0x2488..=0x2490
| 0x24F5..=0x24FE
| 0x2780..=0x2789
| 0x278A..=0x2793
| 0x24FF
)
}
fn str_find<'h>(s: &HeapRead<'h, str>, args: ArgValues, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<Value> {
let str_len = s.get(vm.heap).chars().count();
let (sub, start, end) = parse_search_args("str.find", str_len, args, vm)?;
let s = s.get(vm.heap);
let slice = slice_string(s, start, end);
let result = match slice.find(&sub) {
Some(pos) => {
let char_pos = slice[..pos].chars().count();
i64::try_from(start + char_pos).unwrap_or(i64::MAX)
}
None => -1,
};
Ok(Value::Int(result))
}
fn str_rfind<'h>(s: &HeapRead<'h, str>, args: ArgValues, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<Value> {
let str_len = s.get(vm.heap).chars().count();
let (sub, start, end) = parse_search_args("str.rfind", str_len, args, vm)?;
let s = s.get(vm.heap);
let slice = slice_string(s, start, end);
let result = match slice.rfind(&sub) {
Some(pos) => {
let char_pos = slice[..pos].chars().count();
i64::try_from(start + char_pos).unwrap_or(i64::MAX)
}
None => -1,
};
Ok(Value::Int(result))
}
fn str_index<'h>(s: &HeapRead<'h, str>, args: ArgValues, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<Value> {
let str_len = s.get(vm.heap).chars().count();
let (sub, start, end) = parse_search_args("str.index", str_len, args, vm)?;
let s = s.get(vm.heap);
let slice = slice_string(s, start, end);
match slice.find(&sub) {
Some(pos) => {
let char_pos = slice[..pos].chars().count();
let result = i64::try_from(start + char_pos).unwrap_or(i64::MAX);
Ok(Value::Int(result))
}
None => Err(ExcType::value_error_substring_not_found()),
}
}
fn str_rindex<'h>(s: &HeapRead<'h, str>, args: ArgValues, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<Value> {
let str_len = s.get(vm.heap).chars().count();
let (sub, start, end) = parse_search_args("str.rindex", str_len, args, vm)?;
let s = s.get(vm.heap);
let slice = slice_string(s, start, end);
match slice.rfind(&sub) {
Some(pos) => {
let char_pos = slice[..pos].chars().count();
let result = i64::try_from(start + char_pos).unwrap_or(i64::MAX);
Ok(Value::Int(result))
}
None => Err(ExcType::value_error_substring_not_found()),
}
}
fn str_count<'h>(s: &HeapRead<'h, str>, args: ArgValues, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<Value> {
let str_len = s.get(vm.heap).chars().count();
let (sub, start, end) = parse_search_args("str.count", str_len, args, vm)?;
let s = s.get(vm.heap);
let slice = slice_string(s, start, end);
let count = if sub.is_empty() {
slice.chars().count() + 1
} else {
slice.matches(&sub).count()
};
let result = i64::try_from(count).unwrap_or(i64::MAX);
Ok(Value::Int(result))
}
fn str_startswith<'h>(
s: &HeapRead<'h, str>,
args: ArgValues,
vm: &mut VM<'h, impl ResourceTracker>,
) -> RunResult<Value> {
str_starts_ends_with(s, "str.startswith", |hay, prefix| hay.starts_with(prefix), args, vm)
}
fn str_endswith<'h>(s: &HeapRead<'h, str>, args: ArgValues, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<Value> {
str_starts_ends_with(s, "str.endswith", |hay, suffix| hay.ends_with(suffix), args, vm)
}
fn str_starts_ends_with<'h>(
s: &HeapRead<'h, str>,
method: &'static str,
matcher: impl Fn(&str, &str) -> bool,
args: ArgValues,
vm: &mut VM<'h, impl ResourceTracker>,
) -> RunResult<Value> {
let str_len = s.get(vm.heap).chars().count();
let pos = args.into_pos_only(method, vm.heap)?;
defer_drop!(pos, vm);
let [affix, rest @ ..] = pos.as_slice() else {
return Err(ExcType::type_error_at_least(method, 1, 0));
};
if rest.len() > 2 {
return Err(ExcType::type_error_at_most(method, 3, pos.len()));
}
let start = match rest.first() {
Some(value) => optional_index(value, 0, str_len, vm)?,
None => 0,
};
let end = match rest.get(1) {
Some(value) => optional_index(value, str_len, str_len, vm)?,
None => str_len,
};
let slice = slice_string(s.get(vm.heap), start, end);
Ok(Value::Bool(affix_matches(affix, slice, method, matcher, vm)?))
}
fn affix_matches(
affix: &Value,
slice: &str,
method: &'static str,
matcher: impl Fn(&str, &str) -> bool,
vm: &VM<'_, impl ResourceTracker>,
) -> RunResult<bool> {
let short_method = method.strip_prefix("str.").unwrap_or(method);
let check = |a: &str| matcher(slice, a);
match affix {
Value::InternString(id) => Ok(check(vm.interns.get_str(*id))),
Value::Ref(heap_id) => match vm.heap.get(*heap_id) {
HeapData::Str(a) => Ok(check(a.as_str())),
HeapData::Tuple(tuple) => {
for item in tuple.as_slice() {
let matched = match item {
Value::InternString(id) => check(vm.interns.get_str(*id)),
Value::Ref(hid) if let HeapData::Str(a) = vm.heap.get(*hid) => check(a.as_str()),
_ => {
return Err(ExcType::type_error_affix_tuple_item(
short_method,
"str",
&item.py_type_name(vm),
));
}
};
if matched {
return Ok(true);
}
}
Ok(false)
}
_ => Err(ExcType::type_error_affix_arg(
short_method,
"str",
&affix.py_type_name(vm),
)),
},
_ => Err(ExcType::type_error_affix_arg(
short_method,
"str",
&affix.py_type_name(vm),
)),
}
}
fn parse_search_args(
method: &str,
str_len: usize,
args: ArgValues,
vm: &mut VM<'_, impl ResourceTracker>,
) -> RunResult<(String, usize, usize)> {
let pos = args.into_pos_only(method, vm.heap)?;
defer_drop!(pos, vm);
match pos.as_slice() {
[sub_value] => {
let sub = extract_string_arg(sub_value, vm)?;
Ok((sub, 0, str_len))
}
[sub_value, start_value] => {
let sub = extract_string_arg(sub_value, vm)?;
let start = optional_index(start_value, 0, str_len, vm)?;
Ok((sub, start, str_len))
}
[sub_value, start_value, end_value] => {
let sub = extract_string_arg(sub_value, vm)?;
let start = optional_index(start_value, 0, str_len, vm)?;
let end = optional_index(end_value, str_len, str_len, vm)?;
Ok((sub, start, end))
}
[] => Err(ExcType::type_error_at_least(method, 1, 0)),
_ => Err(ExcType::type_error_at_most(method, 3, pos.len())),
}
}
fn extract_string_arg(value: &Value, vm: &mut VM<'_, impl ResourceTracker>) -> RunResult<String> {
match value {
Value::InternString(id) => Ok(vm.interns.get_str(*id).to_owned()),
Value::Ref(heap_id) => {
if let HeapData::Str(s) = vm.heap.get(*heap_id) {
Ok(s.as_str().to_owned())
} else {
Err(ExcType::type_error("expected str"))
}
}
_ => Err(ExcType::type_error("expected str")),
}
}
fn extract_int_arg(value: &Value, vm: &mut VM<'_, impl ResourceTracker>) -> RunResult<i64> {
match value {
Value::Int(i) => Ok(*i),
Value::Ref(heap_id) => {
if let HeapData::LongInt(li) = vm.heap.get(*heap_id) {
li.to_i64().ok_or_else(|| ExcType::type_error("integer too large"))
} else {
Err(ExcType::type_error("expected int"))
}
}
_ => Err(ExcType::type_error("expected int")),
}
}
fn optional_index(
value: &Value,
default: usize,
str_len: usize,
vm: &mut VM<'_, impl ResourceTracker>,
) -> RunResult<usize> {
match value {
Value::None => Ok(default),
Value::Int(i) => Ok(normalize_sequence_index(*i, str_len)),
Value::Bool(b) => Ok(normalize_sequence_index(i64::from(*b), str_len)),
Value::Ref(heap_id) if let HeapData::LongInt(li) = vm.heap.get(*heap_id) => {
let i = li.to_i64().ok_or_else(|| ExcType::type_error("integer too large"))?;
Ok(normalize_sequence_index(i, str_len))
}
_ => Err(ExcType::type_error_slice_indices()),
}
}
fn slice_string(s: &str, start: usize, end: usize) -> &str {
if start >= end {
return "";
}
let mut start_byte = s.len();
let mut end_byte = s.len();
for (char_idx, (byte_idx, _)) in s.char_indices().enumerate() {
if char_idx == start {
start_byte = byte_idx;
}
if char_idx == end {
end_byte = byte_idx;
break;
}
}
&s[start_byte..end_byte]
}
fn str_strip<'h>(s: &HeapRead<'h, str>, args: ArgValues, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<Value> {
let chars = parse_strip_arg("str.strip", args, vm)?;
let s = s.get(vm.heap);
let result = match &chars {
Some(c) => s.trim_matches(|ch| c.contains(ch)).to_owned(),
None => s.trim().to_owned(),
};
Ok(allocate_string(result, vm.heap)?)
}
fn str_lstrip<'h>(s: &HeapRead<'h, str>, args: ArgValues, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<Value> {
let chars = parse_strip_arg("str.lstrip", args, vm)?;
let s = s.get(vm.heap);
let result = match &chars {
Some(c) => s.trim_start_matches(|ch| c.contains(ch)).to_owned(),
None => s.trim_start().to_owned(),
};
Ok(allocate_string(result, vm.heap)?)
}
fn str_rstrip<'h>(s: &HeapRead<'h, str>, args: ArgValues, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<Value> {
let chars = parse_strip_arg("str.rstrip", args, vm)?;
let s = s.get(vm.heap);
let result = match &chars {
Some(c) => s.trim_end_matches(|ch| c.contains(ch)).to_owned(),
None => s.trim_end().to_owned(),
};
Ok(allocate_string(result, vm.heap)?)
}
fn parse_strip_arg(method: &str, args: ArgValues, vm: &mut VM<'_, impl ResourceTracker>) -> RunResult<Option<String>> {
let value = args.get_zero_one_arg(method, vm.heap)?;
match value {
None => Ok(None),
Some(Value::None) => Ok(None), Some(v) => {
defer_drop!(v, vm);
let result = extract_string_arg(v, vm)?;
Ok(Some(result))
}
}
}
fn str_removeprefix<'h>(
s: &HeapRead<'h, str>,
args: ArgValues,
vm: &mut VM<'h, impl ResourceTracker>,
) -> RunResult<Value> {
let prefix_value = args.get_one_arg("str.removeprefix", vm.heap)?;
defer_drop!(prefix_value, vm);
let prefix = extract_string_arg(prefix_value, vm)?;
let s = s.get(vm.heap);
let result = s.strip_prefix(&prefix).unwrap_or(s).to_owned();
Ok(allocate_string(result, vm.heap)?)
}
fn str_removesuffix<'h>(
s: &HeapRead<'h, str>,
args: ArgValues,
vm: &mut VM<'h, impl ResourceTracker>,
) -> RunResult<Value> {
let suffix_value = args.get_one_arg("str.removesuffix", vm.heap)?;
defer_drop!(suffix_value, vm);
let suffix = extract_string_arg(suffix_value, vm)?;
let s = s.get(vm.heap);
let result = s.strip_suffix(&suffix).unwrap_or(s).to_owned();
Ok(allocate_string(result, vm.heap)?)
}
fn str_split<'h>(s: &HeapRead<'h, str>, args: ArgValues, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<Value> {
let SplitArgs { sep, maxsplit } = SplitArgs::from_args(args, vm)?;
let (sep, maxsplit) = coerce_split_args(sep, maxsplit, vm)?;
let s = s.get(vm.heap);
let parts: Vec<&str> = match &sep {
Some(sep) => {
if sep.is_empty() {
return Err(ExcType::value_error_empty_separator());
}
if maxsplit < 0 {
s.split(sep.as_str()).collect()
} else {
let max = usize::try_from(maxsplit).unwrap_or(usize::MAX);
s.splitn(max.saturating_add(1), sep.as_str()).collect()
}
}
None => {
if maxsplit < 0 {
s.split_whitespace().collect()
} else {
let max = usize::try_from(maxsplit).unwrap_or(usize::MAX);
split_whitespace_n(s, max)
}
}
};
let mut list_items = Vec::with_capacity(parts.len());
for part in parts {
vm.heap.check_time()?;
list_items.push(allocate_string(part, vm.heap)?);
}
let list = super::List::new(list_items);
let heap_id = vm.heap.allocate(HeapData::List(list))?;
Ok(Value::Ref(heap_id))
}
fn str_rsplit<'h>(s: &HeapRead<'h, str>, args: ArgValues, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<Value> {
let RsplitArgs { sep, maxsplit } = RsplitArgs::from_args(args, vm)?;
let (sep, maxsplit) = coerce_split_args(sep, maxsplit, vm)?;
let s = s.get(vm.heap);
let parts: Vec<&str> = match &sep {
Some(sep) => {
if sep.is_empty() {
return Err(ExcType::value_error_empty_separator());
}
if maxsplit < 0 {
s.rsplit(sep.as_str()).collect::<Vec<_>>().into_iter().rev().collect()
} else {
let max = usize::try_from(maxsplit).unwrap_or(usize::MAX);
let mut parts: Vec<_> = s.rsplitn(max.saturating_add(1), sep.as_str()).collect();
parts.reverse();
parts
}
}
None => {
if maxsplit < 0 {
s.split_whitespace().collect()
} else {
let max = usize::try_from(maxsplit).unwrap_or(usize::MAX);
rsplit_whitespace_n(s, max)
}
}
};
let mut list_items = Vec::with_capacity(parts.len());
for part in parts {
vm.heap.check_time()?;
list_items.push(allocate_string(part, vm.heap)?);
}
let list = super::List::new(list_items);
let heap_id = vm.heap.allocate(HeapData::List(list))?;
Ok(Value::Ref(heap_id))
}
fn coerce_split_args(
sep: Value,
maxsplit: Value,
vm: &mut VM<'_, impl ResourceTracker>,
) -> RunResult<(Option<String>, i64)> {
defer_drop!(sep, vm);
defer_drop!(maxsplit, vm);
let sep = match sep {
Value::None => None,
_ => Some(extract_string_arg(sep, vm)?),
};
let maxsplit = extract_int_arg(maxsplit, vm)?;
Ok((sep, maxsplit))
}
#[derive(FromArgs)]
#[from_args(name = "split")]
struct SplitArgs {
#[from_args(default = Value::None)]
sep: Value,
#[from_args(default = Value::Int(-1))]
maxsplit: Value,
}
#[derive(FromArgs)]
#[from_args(name = "rsplit")]
struct RsplitArgs {
#[from_args(default = Value::None)]
sep: Value,
#[from_args(default = Value::Int(-1))]
maxsplit: Value,
}
fn split_whitespace_n(s: &str, maxsplit: usize) -> Vec<&str> {
let mut parts = Vec::new();
let mut remaining = s.trim_start();
let mut count = 0;
while !remaining.is_empty() && count < maxsplit {
if let Some(end) = remaining.find(|c: char| c.is_whitespace()) {
parts.push(&remaining[..end]);
remaining = remaining[end..].trim_start();
count += 1;
} else {
break;
}
}
if !remaining.is_empty() {
parts.push(remaining);
}
parts
}
fn rsplit_whitespace_n(s: &str, maxsplit: usize) -> Vec<&str> {
let mut parts = Vec::new();
let mut remaining = s.trim_end();
let mut count = 0;
while !remaining.is_empty() && count < maxsplit {
if let Some(start) = remaining.rfind(|c: char| c.is_whitespace()) {
let ws_len = remaining[start..].chars().next().unwrap().len_utf8();
parts.push(&remaining[start + ws_len..]);
remaining = remaining[..start].trim_end();
count += 1;
} else {
break;
}
}
if !remaining.is_empty() {
parts.push(remaining);
}
parts.reverse();
parts
}
fn str_splitlines<'h>(
s: &HeapRead<'h, str>,
args: ArgValues,
vm: &mut VM<'h, impl ResourceTracker>,
) -> RunResult<Value> {
let keepends = parse_splitlines_args(args, vm)?;
let s = s.get(vm.heap);
let mut lines = Vec::new();
let mut start = 0;
let bytes = s.as_bytes();
let len = bytes.len();
while start < len {
vm.heap.check_time()?;
let mut end = start;
let mut line_end = start;
while end < len {
match bytes[end] {
b'\n' => {
line_end = end;
end += 1;
break;
}
b'\r' => {
line_end = end;
end += 1;
if end < len && bytes[end] == b'\n' {
end += 1;
}
break;
}
_ => {
end += 1;
line_end = end;
}
}
}
let line = if keepends { &s[start..end] } else { &s[start..line_end] };
lines.push(allocate_string(line, vm.heap)?);
start = end;
}
let list = super::List::new(lines);
let heap_id = vm.heap.allocate(HeapData::List(list))?;
Ok(Value::Ref(heap_id))
}
fn parse_splitlines_args(args: ArgValues, vm: &mut VM<'_, impl ResourceTracker>) -> RunResult<bool> {
let SplitlinesArgs { keepends } = SplitlinesArgs::from_args(args, vm)?;
let result = keepends.as_ref().is_some_and(value_is_truthy);
keepends.drop_with_heap(vm.heap);
Ok(result)
}
#[derive(FromArgs)]
#[from_args(name = "splitlines", at_most_total)]
struct SplitlinesArgs {
#[from_args(default)]
keepends: Option<Value>,
}
fn value_is_truthy(v: &Value) -> bool {
match v {
Value::Bool(b) => *b,
Value::Int(i) => *i != 0,
Value::None => false,
_ => true, }
}
fn str_partition<'h>(
s: &HeapRead<'h, str>,
args: ArgValues,
vm: &mut VM<'h, impl ResourceTracker>,
) -> RunResult<Value> {
let sep_value = args.get_one_arg("str.partition", vm.heap)?;
defer_drop!(sep_value, vm);
let sep = extract_string_arg(sep_value, vm)?;
if sep.is_empty() {
return Err(ExcType::value_error_empty_separator());
}
let s = s.get(vm.heap);
let (before, sep_found, after) = match s.find(&sep) {
Some(pos) => (&s[..pos], &sep[..], &s[pos + sep.len()..]),
None => (s, "", ""),
};
let before_val = allocate_string(before, vm.heap)?;
let sep_val = allocate_string(sep_found, vm.heap)?;
let after_val = allocate_string(after, vm.heap)?;
Ok(super::allocate_tuple(
smallvec![before_val, sep_val, after_val],
vm.heap,
)?)
}
fn str_rpartition<'h>(
s: &HeapRead<'h, str>,
args: ArgValues,
vm: &mut VM<'h, impl ResourceTracker>,
) -> RunResult<Value> {
let sep_value = args.get_one_arg("str.rpartition", vm.heap)?;
defer_drop!(sep_value, vm);
let sep = extract_string_arg(sep_value, vm)?;
if sep.is_empty() {
return Err(ExcType::value_error_empty_separator());
}
let s = s.get(vm.heap);
let (before, sep_found, after) = match s.rfind(&sep) {
Some(pos) => (&s[..pos], &sep[..], &s[pos + sep.len()..]),
None => ("", "", s),
};
let before_val = allocate_string(before, vm.heap)?;
let sep_val = allocate_string(sep_found, vm.heap)?;
let after_val = allocate_string(after, vm.heap)?;
Ok(super::allocate_tuple(
smallvec![before_val, sep_val, after_val],
vm.heap,
)?)
}
fn str_replace<'h>(s: &HeapRead<'h, str>, args: ArgValues, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<Value> {
let (old, new, count) = parse_replace_args("str.replace", args, vm)?;
let s = s.get(vm.heap);
check_replace_size(s.len(), old.len(), new.len(), count, vm.heap.tracker())?;
let result = if count < 0 {
s.replace(&old, &new)
} else {
let n = usize::try_from(count).unwrap_or(usize::MAX);
s.replacen(&old, &new, n)
};
Ok(allocate_string(result, vm.heap)?)
}
fn parse_replace_args(
_method: &str,
args: ArgValues,
vm: &mut VM<'_, impl ResourceTracker>,
) -> RunResult<(String, String, i64)> {
let ReplaceArgs { old, new, count } = ReplaceArgs::from_args(args, vm)?;
defer_drop!(old, vm);
defer_drop!(new, vm);
defer_drop!(count, vm);
let old_s = extract_string_arg(old, vm)?;
let new_s = extract_string_arg(new, vm)?;
let count_i = extract_int_arg(count, vm)?;
Ok((old_s, new_s, count_i))
}
#[derive(FromArgs)]
#[from_args(name = "replace")]
struct ReplaceArgs {
#[from_args(pos_only)]
old: Value,
#[from_args(pos_only)]
new: Value,
#[from_args(default = Value::Int(-1))]
count: Value,
}
fn str_center<'h>(s: &HeapRead<'h, str>, args: ArgValues, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<Value> {
let (width, fillchar) = parse_justify_args("str.center", args, vm)?;
let s = s.get(vm.heap);
let len = s.chars().count();
if width <= len {
Ok(allocate_string(s, vm.heap)?)
} else {
let total_pad = width - len;
let capacity = s.len().saturating_add(total_pad.saturating_mul(fillchar.len_utf8()));
let mut builder = StringBuilder::with_capacity(capacity, vm.heap.tracker())?;
let left_pad = total_pad / 2;
let right_pad = total_pad - left_pad;
for _ in 0..left_pad {
builder.push(fillchar)?;
}
builder.push_str(s)?;
for _ in 0..right_pad {
builder.push(fillchar)?;
}
builder.finish(vm.heap)
}
}
fn str_ljust<'h>(s: &HeapRead<'h, str>, args: ArgValues, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<Value> {
let (width, fillchar) = parse_justify_args("str.ljust", args, vm)?;
let s = s.get(vm.heap);
let len = s.chars().count();
if width <= len {
Ok(allocate_string(s, vm.heap)?)
} else {
let pad = width - len;
let capacity = s.len().saturating_add(pad.saturating_mul(fillchar.len_utf8()));
let mut builder = StringBuilder::with_capacity(capacity, vm.heap.tracker())?;
builder.push_str(s)?;
for _ in 0..pad {
builder.push(fillchar)?;
}
builder.finish(vm.heap)
}
}
fn str_rjust<'h>(s: &HeapRead<'h, str>, args: ArgValues, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<Value> {
let (width, fillchar) = parse_justify_args("str.rjust", args, vm)?;
let s = s.get(vm.heap);
let len = s.chars().count();
if width <= len {
Ok(allocate_string(s, vm.heap)?)
} else {
let pad = width - len;
let capacity = s.len().saturating_add(pad.saturating_mul(fillchar.len_utf8()));
let mut builder = StringBuilder::with_capacity(capacity, vm.heap.tracker())?;
for _ in 0..pad {
builder.push(fillchar)?;
}
builder.push_str(s)?;
builder.finish(vm.heap)
}
}
fn parse_justify_args(
method: &str,
args: ArgValues,
vm: &mut VM<'_, impl ResourceTracker>,
) -> RunResult<(usize, char)> {
let pos = args.into_pos_only(method, vm.heap)?;
defer_drop!(pos, vm);
match pos.as_slice() {
[width_value] => {
let w = extract_int_arg(width_value, vm)?;
let width = if w < 0 {
0
} else {
usize::try_from(w).unwrap_or(usize::MAX)
};
Ok((width, ' '))
}
[width_value, fillchar_value] => {
let w = extract_int_arg(width_value, vm)?;
let width = if w < 0 {
0
} else {
usize::try_from(w).unwrap_or(usize::MAX)
};
let fill_str = extract_string_arg(fillchar_value, vm)?;
if fill_str.chars().count() != 1 {
return Err(ExcType::type_error_fillchar_must_be_single_char());
}
Ok((width, fill_str.chars().next().unwrap()))
}
[] => Err(ExcType::type_error_at_least(method, 1, 0)),
_ => Err(ExcType::type_error_at_most(method, 2, pos.len())),
}
}
fn str_zfill<'h>(s: &HeapRead<'h, str>, args: ArgValues, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<Value> {
let width_value = args.get_one_arg("str.zfill", vm.heap)?;
defer_drop!(width_value, vm);
let width_i64 = extract_int_arg(width_value, vm)?;
let width = if width_i64 < 0 {
0
} else {
usize::try_from(width_i64).unwrap_or(usize::MAX)
};
let s = s.get(vm.heap);
let len = s.chars().count();
if width <= len {
Ok(allocate_string(s, vm.heap)?)
} else {
let pad = width - len;
let capacity = s.len().saturating_add(pad);
let mut builder = StringBuilder::with_capacity(capacity, vm.heap.tracker())?;
let mut chars = s.chars();
let first = chars.next();
if matches!(first, Some('+' | '-')) {
builder.push(first.unwrap())?;
for _ in 0..pad {
builder.push('0')?;
}
for c in chars {
builder.push(c)?;
}
} else {
for _ in 0..pad {
builder.push('0')?;
}
builder.push_str(s)?;
}
builder.finish(vm.heap)
}
}
fn str_expandtabs<'h>(
s: &HeapRead<'h, str>,
args: ArgValues,
vm: &mut VM<'h, impl ResourceTracker>,
) -> RunResult<Value> {
let ExpandtabsArgs { tabsize } = ExpandtabsArgs::from_args(args, vm)?;
let tabsize = match tabsize {
None => 8,
Some(val) => {
let result_int = extract_int_arg(&val, vm)?;
val.drop_with_heap(vm.heap);
if result_int < 0 {
0
} else {
usize::try_from(result_int).unwrap_or(usize::MAX)
}
}
};
let s = s.get(vm.heap);
let mut builder = StringBuilder::new(vm.heap.tracker());
let mut column = 0;
for c in s.chars() {
if c == '\t' {
if tabsize > 0 {
let spaces = tabsize - (column % tabsize);
for _ in 0..spaces {
builder.push(' ')?;
}
column += spaces;
}
} else {
builder.push(c)?;
if c == '\n' || c == '\r' {
column = 0;
} else {
column += 1;
}
}
}
builder.finish(vm.heap)
}
#[derive(FromArgs)]
#[from_args(name = "expandtabs", at_most_total)]
struct ExpandtabsArgs {
#[from_args(default)]
tabsize: Option<Value>,
}
fn str_encode<'h>(s: &HeapRead<'h, str>, args: ArgValues, vm: &mut VM<'h, impl ResourceTracker>) -> RunResult<Value> {
let EncodeArgs { encoding, errors } = EncodeArgs::from_args(args, vm)?;
defer_drop!(encoding, vm);
defer_drop!(errors, vm);
let encoding = encoding.as_ref().map_or("utf-8", |e| e.as_str(vm));
let errors = errors.as_ref().map_or("strict", |e| e.as_str(vm));
let codec = Codec::find(encoding).ok_or_else(|| ExcType::lookup_error_unknown_encoding(encoding))?;
let bytes = codec.encode(s.get(vm.heap), errors, vm.heap.tracker())?;
let heap_id = vm.heap.allocate(HeapData::Bytes(Bytes::new(bytes)))?;
Ok(Value::Ref(heap_id))
}
#[derive(FromArgs)]
#[from_args(name = "encode", at_most_total, bad_arg_named)]
struct EncodeArgs {
#[from_args(default)]
encoding: Option<StrArg>,
#[from_args(default)]
errors: Option<StrArg>,
}
fn str_isidentifier(s: &str) -> bool {
if s.is_empty() {
return false;
}
let mut chars = s.chars();
let first = chars.next().unwrap();
if !is_xid_start(first) && first != '_' {
return false;
}
chars.all(is_xid_continue)
}
fn is_xid_start(c: char) -> bool {
c.is_alphabetic()
}
fn is_xid_continue(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
fn str_istitle(s: &str) -> bool {
if s.is_empty() {
return false;
}
let mut prev_cased = false;
let mut has_cased = false;
for c in s.chars() {
if c.is_uppercase() {
if prev_cased {
return false;
}
prev_cased = true;
has_cased = true;
} else if c.is_lowercase() {
if !prev_cased {
return false;
}
prev_cased = true;
has_cased = true;
} else {
prev_cased = false;
}
}
has_cased
}