Skip to main content

rustpython_common/
str.rs

1// spell-checker:ignore uncomputed
2use crate::atomic::{OncePtr, PyAtomic, Radium};
3use crate::format::CharLen;
4use crate::wtf8::{CodePoint, Wtf8, Wtf8Buf};
5use crate::wtf8_index::Wtf8Index;
6use alloc::borrow::Cow;
7use ascii::{AsciiChar, AsciiStr, AsciiString};
8use core::fmt;
9use core::ops::{Bound, RangeBounds};
10use core::sync::atomic::Ordering::Relaxed;
11
12#[allow(non_camel_case_types)]
13pub type wchar_t = cfg_select! {
14    target_arch = "wasm32" => u32,
15    _ => libc::wchar_t,
16};
17
18/// Utf8 + state.ascii (+ PyUnicode_Kind in future)
19#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
20pub enum StrKind {
21    Ascii,
22    Utf8,
23    Wtf8,
24}
25
26impl core::ops::BitOr for StrKind {
27    type Output = Self;
28
29    fn bitor(self, other: Self) -> Self {
30        match (self, other) {
31            (Self::Wtf8, _) | (_, Self::Wtf8) => Self::Wtf8,
32            (Self::Utf8, _) | (_, Self::Utf8) => Self::Utf8,
33            (Self::Ascii, Self::Ascii) => Self::Ascii,
34        }
35    }
36}
37
38impl StrKind {
39    #[must_use]
40    pub const fn is_ascii(&self) -> bool {
41        matches!(self, Self::Ascii)
42    }
43
44    #[must_use]
45    pub const fn is_utf8(&self) -> bool {
46        matches!(self, Self::Ascii | Self::Utf8)
47    }
48
49    #[inline(always)]
50    #[must_use]
51    pub fn can_encode(&self, code: CodePoint) -> bool {
52        match self {
53            Self::Ascii => code.is_ascii(),
54            Self::Utf8 => code.to_char().is_some(),
55            Self::Wtf8 => true,
56        }
57    }
58}
59
60pub trait DeduceStrKind {
61    fn str_kind(&self) -> StrKind;
62}
63
64impl DeduceStrKind for str {
65    fn str_kind(&self) -> StrKind {
66        if self.is_ascii() {
67            StrKind::Ascii
68        } else {
69            StrKind::Utf8
70        }
71    }
72}
73
74impl DeduceStrKind for Wtf8 {
75    fn str_kind(&self) -> StrKind {
76        if self.is_ascii() {
77            StrKind::Ascii
78        } else if self.is_utf8() {
79            StrKind::Utf8
80        } else {
81            StrKind::Wtf8
82        }
83    }
84}
85
86impl DeduceStrKind for String {
87    fn str_kind(&self) -> StrKind {
88        (**self).str_kind()
89    }
90}
91
92impl DeduceStrKind for Wtf8Buf {
93    fn str_kind(&self) -> StrKind {
94        (**self).str_kind()
95    }
96}
97
98impl<T: DeduceStrKind + ?Sized> DeduceStrKind for &T {
99    fn str_kind(&self) -> StrKind {
100        (**self).str_kind()
101    }
102}
103
104impl<T: DeduceStrKind + ?Sized> DeduceStrKind for Box<T> {
105    fn str_kind(&self) -> StrKind {
106        (**self).str_kind()
107    }
108}
109
110#[derive(Debug)]
111pub enum PyKindStr<'a> {
112    Ascii(&'a AsciiStr),
113    Utf8(&'a str),
114    Wtf8(&'a Wtf8),
115}
116
117/// How far from an end an index is resolved by walking rather than by building
118/// the code point index.
119///
120/// PyPy spells this `MAX_UNROLL_NEXT_CODEPOINT_POS`, in a guard that also asks
121/// the JIT whether the index is a constant, so that the walk unrolls. There is
122/// no JIT here to ask, and the walk is short rather than free -- but four steps
123/// still beat a pass over the whole buffer, and skipping the build is what
124/// keeps `s[0]` and `s[1:-1]` on a long string from paying for a table.
125const MAX_WALK_TO_INDEX: usize = 4;
126
127#[derive(Debug, Clone)]
128pub struct StrData {
129    data: Box<Wtf8>,
130    kind: StrKind,
131    len: StrLen,
132    index: Wtf8IndexSlot,
133}
134
135/// A [`Wtf8Index`] built on first use.
136///
137/// The table is a pure function of `data`, so publishing it races benignly: a
138/// thread that loses the exchange drops its own copy and reads the winner's.
139#[derive(Default)]
140struct Wtf8IndexSlot(OncePtr<Wtf8Index>);
141
142impl Wtf8IndexSlot {
143    #[inline(always)]
144    fn new() -> Self {
145        Self(OncePtr::new())
146    }
147
148    #[inline]
149    fn get_or_build(&self, data: &Wtf8, char_len: usize) -> &Wtf8Index {
150        let index = self
151            .0
152            .get_or_init(|| Box::new(Wtf8Index::new(data, char_len)));
153        // The slot owns the table, never replaces it, and outlives the borrow.
154        unsafe { index.as_ref() }
155    }
156}
157
158impl fmt::Debug for Wtf8IndexSlot {
159    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
160        match self.0.get() {
161            Some(_) => f.write_str("<built>"),
162            None => f.write_str("<unbuilt>"),
163        }
164    }
165}
166
167impl Clone for Wtf8IndexSlot {
168    /// A fresh slot: the clone copies the buffer, so it has to index that copy,
169    /// and the table is rebuilt on demand rather than eagerly here.
170    fn clone(&self) -> Self {
171        Self::new()
172    }
173}
174
175impl Drop for Wtf8IndexSlot {
176    fn drop(&mut self) {
177        if let Some(index) = self.0.get() {
178            drop(unsafe { Box::from_raw(index.as_ptr()) });
179        }
180    }
181}
182
183struct StrLen(PyAtomic<usize>);
184
185impl From<usize> for StrLen {
186    #[inline(always)]
187    fn from(value: usize) -> Self {
188        Self(Radium::new(value))
189    }
190}
191
192impl fmt::Debug for StrLen {
193    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
194        let len = self.0.load(Relaxed);
195        if len == usize::MAX {
196            f.write_str("<uncomputed>")
197        } else {
198            len.fmt(f)
199        }
200    }
201}
202
203impl StrLen {
204    #[inline(always)]
205    fn zero() -> Self {
206        0usize.into()
207    }
208
209    #[inline(always)]
210    fn uncomputed() -> Self {
211        usize::MAX.into()
212    }
213}
214
215impl Clone for StrLen {
216    fn clone(&self) -> Self {
217        Self(self.0.load(Relaxed).into())
218    }
219}
220
221impl Default for StrData {
222    fn default() -> Self {
223        Self {
224            data: <Box<Wtf8>>::default(),
225            kind: StrKind::Ascii,
226            len: StrLen::zero(),
227            index: Wtf8IndexSlot::new(),
228        }
229    }
230}
231
232impl From<Box<Wtf8>> for StrData {
233    fn from(value: Box<Wtf8>) -> Self {
234        // doing the check is ~10x faster for ascii, and is actually only 2% slower worst case for
235        // non-ascii; see https://github.com/RustPython/RustPython/pull/2586#issuecomment-844611532
236        let kind = value.str_kind();
237        unsafe { Self::new_str_unchecked(value, kind) }
238    }
239}
240
241impl From<Box<str>> for StrData {
242    #[inline]
243    fn from(value: Box<str>) -> Self {
244        // doing the check is ~10x faster for ascii, and is actually only 2% slower worst case for
245        // non-ascii; see https://github.com/RustPython/RustPython/pull/2586#issuecomment-844611532
246        let kind = value.str_kind();
247        unsafe { Self::new_str_unchecked(value.into(), kind) }
248    }
249}
250
251impl From<Box<AsciiStr>> for StrData {
252    #[inline]
253    fn from(value: Box<AsciiStr>) -> Self {
254        Self {
255            len: value.len().into(),
256            data: value.into(),
257            kind: StrKind::Ascii,
258            index: Wtf8IndexSlot::new(),
259        }
260    }
261}
262
263impl From<AsciiChar> for StrData {
264    fn from(ch: AsciiChar) -> Self {
265        AsciiString::from(ch).into_boxed_ascii_str().into()
266    }
267}
268
269impl From<char> for StrData {
270    fn from(ch: char) -> Self {
271        if let Ok(ch) = ascii::AsciiChar::from_ascii(ch) {
272            ch.into()
273        } else {
274            Self {
275                data: ch.to_string().into(),
276                kind: StrKind::Utf8,
277                len: 1.into(),
278                index: Wtf8IndexSlot::new(),
279            }
280        }
281    }
282}
283
284impl From<CodePoint> for StrData {
285    fn from(ch: CodePoint) -> Self {
286        if let Some(ch) = ch.to_char() {
287            ch.into()
288        } else {
289            Self {
290                data: Wtf8Buf::from(ch).into(),
291                kind: StrKind::Wtf8,
292                len: 1.into(),
293                index: Wtf8IndexSlot::new(),
294            }
295        }
296    }
297}
298
299impl StrData {
300    /// # Safety
301    ///
302    /// Given `bytes` must be valid data for given `kind`
303    #[must_use]
304    pub unsafe fn new_str_unchecked(data: Box<Wtf8>, kind: StrKind) -> Self {
305        let len = match kind {
306            StrKind::Ascii => data.len().into(),
307            _ => StrLen::uncomputed(),
308        };
309        Self {
310            data,
311            kind,
312            len,
313            index: Wtf8IndexSlot::new(),
314        }
315    }
316
317    /// # Safety
318    ///
319    /// `char_len` must be accurate.
320    #[must_use]
321    pub unsafe fn new_with_char_len(data: Box<Wtf8>, kind: StrKind, char_len: usize) -> Self {
322        Self {
323            data,
324            kind,
325            len: char_len.into(),
326            index: Wtf8IndexSlot::new(),
327        }
328    }
329
330    #[inline]
331    pub const fn as_wtf8(&self) -> &Wtf8 {
332        &self.data
333    }
334
335    // TODO: rename to to_str
336    #[inline]
337    pub fn as_str(&self) -> Option<&str> {
338        self.kind
339            .is_utf8()
340            .then(|| unsafe { core::str::from_utf8_unchecked(self.data.as_bytes()) })
341    }
342
343    pub fn as_ascii(&self) -> Option<&AsciiStr> {
344        self.kind
345            .is_ascii()
346            .then(|| unsafe { AsciiStr::from_ascii_unchecked(self.data.as_bytes()) })
347    }
348
349    pub const fn kind(&self) -> StrKind {
350        self.kind
351    }
352
353    #[inline]
354    pub fn as_str_kind(&self) -> PyKindStr<'_> {
355        match self.kind {
356            StrKind::Ascii => {
357                PyKindStr::Ascii(unsafe { AsciiStr::from_ascii_unchecked(self.data.as_bytes()) })
358            }
359            StrKind::Utf8 => {
360                PyKindStr::Utf8(unsafe { core::str::from_utf8_unchecked(self.data.as_bytes()) })
361            }
362            StrKind::Wtf8 => PyKindStr::Wtf8(&self.data),
363        }
364    }
365
366    #[inline]
367    pub fn len(&self) -> usize {
368        self.data.len()
369    }
370
371    pub fn is_empty(&self) -> bool {
372        self.data.is_empty()
373    }
374
375    #[inline]
376    pub fn char_len(&self) -> usize {
377        match self.len.0.load(Relaxed) {
378            usize::MAX => self._compute_char_len(),
379            len => len,
380        }
381    }
382
383    #[cold]
384    fn _compute_char_len(&self) -> usize {
385        let len = if let Some(s) = self.as_str() {
386            // utf8 chars().count() is optimized
387            s.chars().count()
388        } else {
389            self.data.code_points().count()
390        };
391        // len cannot be usize::MAX, since vec.capacity() < sys.maxsize
392        self.len.0.store(len, Relaxed);
393        len
394    }
395
396    /// The byte offset the `index`-th code point starts at.
397    ///
398    /// An `index` at or past the end answers the buffer's byte length, so a
399    /// caller walking to a bound does not have to special-case it.
400    ///
401    /// O(1), but the first call on a non-ASCII string builds an index over the
402    /// whole buffer, so a caller that resolves a single index and stops is
403    /// better served by [`Self::nth_char`].
404    pub fn char_index_to_byte(&self, index: usize) -> usize {
405        // For ASCII the two units coincide, and the table would be a Nth entry
406        // saying N.
407        if self.kind.is_ascii() {
408            return index.min(self.data.len());
409        }
410        let char_len = self.char_len();
411        if index >= char_len {
412            return self.data.len();
413        }
414        self.index
415            .get_or_build(&self.data, char_len)
416            .byte_offset(&self.data, index)
417    }
418
419    /// The byte offset of code point `index`, for a caller that resolves one
420    /// index and stops.
421    ///
422    /// Building the table costs a pass over the whole buffer, so it is worth it
423    /// only for a caller that comes back; an index within
424    /// [`MAX_WALK_TO_INDEX`] steps of either end is cheaper to walk to, and
425    /// walking keeps `s[0]` on a long string from paying for a table it will
426    /// never use again. Anything further in builds, on the reasoning that a
427    /// string indexed once in the middle tends to be indexed again.
428    fn char_index_to_byte_once(&self, index: usize) -> usize {
429        if index <= MAX_WALK_TO_INDEX {
430            return self
431                .data
432                .code_point_indices()
433                .nth(index)
434                .map_or(self.data.len(), |(byte, _)| byte);
435        }
436        let from_end = self.char_len() - index;
437        if from_end <= MAX_WALK_TO_INDEX {
438            return self
439                .data
440                .code_point_indices()
441                .nth_back(from_end - 1)
442                .map_or(self.data.len(), |(byte, _)| byte);
443        }
444        self.char_index_to_byte(index)
445    }
446
447    /// The byte range spanned by the code points in `range`, whose end must not
448    /// exceed the string's code point count.
449    ///
450    /// A range that reaches within [`MAX_WALK_TO_INDEX`] of *both* ends is
451    /// walked to for the same reason a single index near one end is -- a slice
452    /// like `s[1:-1]` should not build a table over the whole string.
453    #[must_use]
454    pub fn char_range_to_bytes(&self, range: core::ops::Range<usize>) -> core::ops::Range<usize> {
455        if self.kind.is_ascii() {
456            return range;
457        }
458        let from_end = self.char_len() - range.end;
459        if range.start <= MAX_WALK_TO_INDEX && from_end <= MAX_WALK_TO_INDEX {
460            // Two walks over disjoint ends, each of at most MAX_WALK_TO_INDEX
461            // steps -- one iterator driven from both sides would have them meet
462            // on a short string.
463            let start = self
464                .data
465                .code_point_indices()
466                .nth(range.start)
467                .map_or(self.data.len(), |(byte, _)| byte);
468            let end = match from_end {
469                0 => self.data.len(),
470                n => self
471                    .data
472                    .code_point_indices()
473                    .nth_back(n - 1)
474                    .map_or(self.data.len(), |(byte, _)| byte),
475            };
476            return start..end;
477        }
478        self.char_index_to_byte(range.start)..self.char_index_to_byte(range.end)
479    }
480
481    /// The character index of the character starting at byte offset `bytepos`,
482    /// the inverse of [`Self::char_index_to_byte`].
483    ///
484    /// `bytepos` must be a character boundary at or before the end.
485    ///
486    /// Logarithmic rather than constant, because the index is keyed the other
487    /// way -- but a search whose bounds came from `char_index_to_byte` has the
488    /// table already, and this is what turns a byte offset back into the answer
489    /// a caller asked for in characters.
490    pub fn byte_to_char_index(&self, bytepos: usize) -> usize {
491        if self.kind.is_ascii() {
492            return bytepos;
493        }
494        let char_len = self.char_len();
495        self.index
496            .get_or_build(&self.data, char_len)
497            .char_index_at_byte(&self.data, bytepos, char_len)
498    }
499
500    pub fn nth_char(&self, index: usize) -> CodePoint {
501        match self.as_str_kind() {
502            PyKindStr::Ascii(s) => s[index].into(),
503            _ => self.data[self.char_index_to_byte_once(index)..]
504                .code_points()
505                .next()
506                .unwrap(),
507        }
508    }
509}
510
511impl core::fmt::Display for StrData {
512    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
513        self.data.fmt(f)
514    }
515}
516
517impl CharLen for StrData {
518    fn char_len(&self) -> usize {
519        self.char_len()
520    }
521}
522
523pub fn try_get_chars(s: &str, range: impl RangeBounds<usize>) -> Option<&str> {
524    let mut chars = s.chars();
525    let start = match range.start_bound() {
526        Bound::Included(&i) => i,
527        Bound::Excluded(&i) => i + 1,
528        Bound::Unbounded => 0,
529    };
530    for _ in 0..start {
531        chars.next()?;
532    }
533    let s = chars.as_str();
534    let range_len = match range.end_bound() {
535        Bound::Included(&i) => i + 1 - start,
536        Bound::Excluded(&i) => i - start,
537        Bound::Unbounded => return Some(s),
538    };
539    char_range_end(s, range_len).map(|end| &s[..end])
540}
541
542pub fn get_chars(s: &str, range: impl RangeBounds<usize>) -> &str {
543    try_get_chars(s, range).unwrap()
544}
545
546#[inline]
547#[must_use]
548pub fn char_range_end(s: &str, n_chars: usize) -> Option<usize> {
549    let i = match n_chars.checked_sub(1) {
550        Some(last_char_index) => {
551            let (index, c) = s.char_indices().nth(last_char_index)?;
552            index + c.len_utf8()
553        }
554        None => 0,
555    };
556    Some(i)
557}
558
559pub fn try_get_codepoints(w: &Wtf8, range: impl RangeBounds<usize>) -> Option<&Wtf8> {
560    let mut chars = w.code_points();
561    let start = match range.start_bound() {
562        Bound::Included(&i) => i,
563        Bound::Excluded(&i) => i + 1,
564        Bound::Unbounded => 0,
565    };
566    for _ in 0..start {
567        chars.next()?;
568    }
569    let s = chars.as_wtf8();
570    let range_len = match range.end_bound() {
571        Bound::Included(&i) => i + 1 - start,
572        Bound::Excluded(&i) => i - start,
573        Bound::Unbounded => return Some(s),
574    };
575    codepoint_range_end(s, range_len).map(|end| &s[..end])
576}
577
578pub fn get_codepoints(w: &Wtf8, range: impl RangeBounds<usize>) -> &Wtf8 {
579    try_get_codepoints(w, range).unwrap()
580}
581
582#[inline]
583#[must_use]
584pub fn codepoint_range_end(s: &Wtf8, n_chars: usize) -> Option<usize> {
585    let i = match n_chars.checked_sub(1) {
586        Some(last_char_index) => {
587            let (index, c) = s.code_point_indices().nth(last_char_index)?;
588            index + c.len_wtf8()
589        }
590        None => 0,
591    };
592    Some(i)
593}
594
595#[must_use]
596/// Returns `None` for a width whose result cannot be allocated.
597pub fn zfill(bytes: &[u8], width: usize) -> Option<Vec<u8>> {
598    if width <= bytes.len() {
599        return Some(bytes.to_vec());
600    }
601    let (sign, s) = match bytes.first() {
602        Some(_sign @ (b'+' | b'-')) => (unsafe { bytes.get_unchecked(..1) }, &bytes[1..]),
603        _ => (&b""[..], bytes),
604    };
605    let mut filled = Vec::new();
606    filled.try_reserve_exact(width).ok()?;
607    filled.extend_from_slice(sign);
608    filled.extend(core::iter::repeat_n(b'0', width - bytes.len()));
609    filled.extend_from_slice(s);
610    Some(filled)
611}
612
613/// Convert a string to ascii compatible, escaping unicode-s into escape
614/// sequences.
615#[must_use]
616pub fn to_ascii(value: &Wtf8) -> AsciiString {
617    let mut ascii = Vec::new();
618    for cp in value.code_points() {
619        if cp.is_ascii() {
620            ascii.push(cp.to_u32() as u8);
621        } else {
622            let c = cp.to_u32();
623            let hex = if c < 0x100 {
624                format!("\\x{c:02x}")
625            } else if c < 0x10000 {
626                format!("\\u{c:04x}")
627            } else {
628                format!("\\U{c:08x}")
629            };
630            ascii.append(&mut hex.into_bytes());
631        }
632    }
633    unsafe { AsciiString::from_ascii_unchecked(ascii) }
634}
635
636#[derive(Clone, Copy)]
637pub struct UnicodeEscapeCodepoint(pub CodePoint);
638
639impl fmt::Display for UnicodeEscapeCodepoint {
640    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
641        let c = self.0.to_u32();
642        if c >= 0x10000 {
643            write!(f, "\\U{c:08x}")
644        } else if c >= 0x100 {
645            write!(f, "\\u{c:04x}")
646        } else {
647            write!(f, "\\x{c:02x}")
648        }
649    }
650}
651
652pub mod levenshtein {
653    pub const MOVE_COST: usize = 2;
654    const CASE_COST: usize = 1;
655    const MAX_STRING_SIZE: usize = 40;
656
657    const fn substitution_cost(mut a: u8, mut b: u8) -> usize {
658        if (a & 31) != (b & 31) {
659            return MOVE_COST;
660        }
661        if a == b {
662            return 0;
663        }
664        if a.is_ascii_uppercase() {
665            a += b'a' - b'A';
666        }
667        if b.is_ascii_uppercase() {
668            b += b'a' - b'A';
669        }
670        if a == b { CASE_COST } else { MOVE_COST }
671    }
672
673    #[must_use]
674    pub fn levenshtein_distance(a: &[u8], b: &[u8], max_cost: usize) -> usize {
675        if a == b {
676            return 0;
677        }
678
679        let (mut a_bytes, mut b_bytes) = (a, b);
680        let (mut a_begin, mut a_end) = (0usize, a.len());
681        let (mut b_begin, mut b_end) = (0usize, b.len());
682
683        while a_end > 0 && b_end > 0 && (a_bytes[a_begin] == b_bytes[b_begin]) {
684            a_begin += 1;
685            b_begin += 1;
686            a_end -= 1;
687            b_end -= 1;
688        }
689        while a_end > 0
690            && b_end > 0
691            && (a_bytes[a_begin + a_end - 1] == b_bytes[b_begin + b_end - 1])
692        {
693            a_end -= 1;
694            b_end -= 1;
695        }
696        if a_end == 0 || b_end == 0 {
697            return (a_end + b_end) * MOVE_COST;
698        }
699        if a_end > MAX_STRING_SIZE || b_end > MAX_STRING_SIZE {
700            return max_cost + 1;
701        }
702
703        if b_end < a_end {
704            core::mem::swap(&mut a_bytes, &mut b_bytes);
705            core::mem::swap(&mut a_begin, &mut b_begin);
706            core::mem::swap(&mut a_end, &mut b_end);
707        }
708
709        if (b_end - a_end) * MOVE_COST > max_cost {
710            return max_cost + 1;
711        }
712
713        let mut buffer = [0usize; MAX_STRING_SIZE];
714
715        for (i, x) in buffer.iter_mut().take(a_end).enumerate() {
716            *x = (i + 1) * MOVE_COST;
717        }
718
719        let mut result = 0usize;
720        for (b_index, b_code) in b_bytes[b_begin..(b_begin + b_end)].iter().enumerate() {
721            result = b_index * MOVE_COST;
722            let mut distance = result;
723            let mut minimum = usize::MAX;
724            for (a_index, a_code) in a_bytes[a_begin..(a_begin + a_end)].iter().enumerate() {
725                let substitute = distance + substitution_cost(*b_code, *a_code);
726                distance = buffer[a_index];
727                let insert_delete = usize::min(result, distance) + MOVE_COST;
728                result = usize::min(insert_delete, substitute);
729
730                buffer[a_index] = result;
731                if result < minimum {
732                    minimum = result;
733                }
734            }
735            if minimum > max_cost {
736                return max_cost + 1;
737            }
738        }
739        result
740    }
741}
742
743/// Replace all tabs in a string with spaces, using the given tab size.
744#[must_use]
745pub fn expandtabs(input: &Wtf8, tab_size: usize) -> Wtf8Buf {
746    // A tab size of zero, which is also where a negative one lands, leaves no
747    // column for a tab to advance to: the tabs come out and nothing else moves.
748    // Going through the arithmetic anyway subtracts the current column from a
749    // tab stop of zero and underflows on the first tab, so the width asked for
750    // next is `usize::MAX`. The bytes version of this already returns here.
751    if tab_size == 0 {
752        return input.code_points().filter(|ch| *ch != '\t').collect();
753    }
754
755    let tab_stop = tab_size;
756    let mut expanded_str = Wtf8Buf::with_capacity(input.len());
757    let mut tab_size = tab_stop;
758    let mut col_count = 0usize;
759    for ch in input.code_points() {
760        if ch == '\t' {
761            let num_spaces = tab_size - col_count;
762            col_count += num_spaces;
763            expanded_str.push_str(&" ".repeat(num_spaces));
764        } else {
765            expanded_str.push(ch);
766            if ch == '\r' || ch == '\n' {
767                col_count = 0;
768                tab_size = 0;
769            } else {
770                col_count += 1;
771            }
772        }
773        if col_count >= tab_size {
774            tab_size += tab_stop;
775        }
776    }
777    expanded_str
778}
779
780/// Creates an [`AsciiStr`][ascii::AsciiStr] from a string literal, throwing a compile error if the
781/// literal isn't actually ascii.
782///
783/// ```compile_fail
784/// # use rustpython_common::str::ascii;
785/// ascii!("I ❤️ Rust & Python");
786/// ```
787#[macro_export]
788macro_rules! ascii {
789    ($x:expr $(,)?) => {{
790        let s = const {
791            let s: &str = $x;
792            assert!(s.is_ascii(), "ascii!() argument is not an ascii string");
793            s
794        };
795        unsafe { $crate::vendored::ascii::AsciiStr::from_ascii_unchecked(s.as_bytes()) }
796    }};
797}
798pub use ascii;
799
800/// The decimal digit value of `ch`, if it has one — CPython's `Py_UNICODE_TODECIMAL`.
801///
802/// Reads the bundled character database, the same source `unicodedata.decimal`
803/// consults, so the two can never disagree and new scripts are picked up with the
804/// next Unicode update.
805#[must_use]
806pub fn char_to_decimal(ch: char) -> Option<u8> {
807    let value = rustpython_unicode::Ucd::new(true).decimal(CodePoint::from(ch))?;
808    u8::try_from(value).ok()
809}
810
811/// Replace Unicode decimal digits with their ASCII equivalents and any Unicode
812/// whitespace with a plain space, so the byte-oriented numeric parsers can read
813/// them. Mirrors CPython's `_PyUnicode_TransformDecimalAndSpaceToASCII`.
814///
815/// The result is always ASCII. Any other non-ASCII character cannot appear in a
816/// numeric literal, so it becomes a `?` and the rest of the string is dropped:
817/// `?` is rejected by every parser at every base, which leaves the caller — the
818/// one that knows the base and owns the original string — to raise the error.
819#[must_use]
820pub fn transform_decimal_and_space_to_ascii(s: &str) -> Cow<'_, str> {
821    if s.is_ascii() {
822        return Cow::Borrowed(s);
823    }
824    let mut out = String::with_capacity(s.len());
825    for c in s.chars() {
826        if (c as u32) < 127 {
827            out.push(c);
828        } else if c.is_whitespace() {
829            out.push(' ');
830        } else if let Some(n) = char_to_decimal(c) {
831            out.push(char::from_digit(n.into(), 10).unwrap());
832        } else {
833            out.push('?');
834            break;
835        }
836    }
837    debug_assert!(out.is_ascii());
838    Cow::Owned(out)
839}
840
841#[cfg(test)]
842mod tests {
843    use super::*;
844
845    #[test]
846    fn char_to_decimal_values() {
847        assert_eq!(char_to_decimal('7'), Some(7));
848        // Decimal digits from any script, including scripts the previous
849        // hand-written table predated.
850        assert_eq!(char_to_decimal('٣'), Some(3)); // U+0663 Arabic-Indic
851        assert_eq!(char_to_decimal('𐴵'), Some(5)); // U+10D35, Unicode 11
852        assert_eq!(char_to_decimal('🯵'), Some(5)); // U+1FBF5, Unicode 13
853        // Numeric but not a decimal digit, so it has no digit value.
854        assert_eq!(char_to_decimal('½'), None);
855        assert_eq!(char_to_decimal('가'), None);
856    }
857
858    #[test]
859    fn transform_decimal_and_space() {
860        // ASCII input is passed through untouched, without allocating.
861        assert!(matches!(
862            transform_decimal_and_space_to_ascii("123"),
863            Cow::Borrowed("123")
864        ));
865        // Decimal digits from any script fold to ASCII.
866        assert_eq!(transform_decimal_and_space_to_ascii("١٢٣"), "123");
867        assert_eq!(transform_decimal_and_space_to_ascii("12३"), "123");
868        assert_eq!(transform_decimal_and_space_to_ascii("1٢3"), "123");
869        // Unicode whitespace folds to a plain space.
870        assert_eq!(transform_decimal_and_space_to_ascii("\u{3000}٣"), " 3");
871        // ASCII characters ride through untouched, whatever they are.
872        assert_eq!(transform_decimal_and_space_to_ascii("0x١f"), "0x1f");
873        assert_eq!(transform_decimal_and_space_to_ascii("-١_٢"), "-1_2");
874        // Anything else poisons the literal and truncates it, so the result stays
875        // ASCII and the caller's parser is guaranteed to reject it.
876        assert_eq!(transform_decimal_and_space_to_ascii("½가"), "?");
877        assert_eq!(transform_decimal_and_space_to_ascii("١٢가٣"), "12?");
878        assert_eq!(transform_decimal_and_space_to_ascii("١\u{7f}"), "1?");
879    }
880
881    #[test]
882    fn get_chars_basic() {
883        let s = "0123456789";
884        assert_eq!(get_chars(s, 3..7), "3456");
885        assert_eq!(get_chars(s, 3..7), &s[3..7]);
886
887        let s = "0유니코드 문자열9";
888        assert_eq!(get_chars(s, 3..7), "코드 문");
889
890        let s = "0😀😃😄😁😆😅😂🤣9";
891        assert_eq!(get_chars(s, 3..7), "😄😁😆😅");
892    }
893
894    fn expandtabs(input: &str, tab_size: usize) -> Wtf8Buf {
895        super::expandtabs(Wtf8::new(input), tab_size)
896    }
897
898    #[test]
899    fn expandtabs_with_zero_tab_size_drops_tabs() {
900        // A tab that follows a character used to subtract that column from a
901        // tab stop of zero, so the width of the run of spaces came out as
902        // `usize::MAX` and the allocation aborted the process.
903        assert_eq!(expandtabs("a\tb", 0), Wtf8Buf::from("ab"));
904        assert_eq!(expandtabs("ab\tcd\tef", 0), Wtf8Buf::from("abcdef"));
905        assert_eq!(expandtabs("a\nb\tc", 0), Wtf8Buf::from("a\nbc"));
906        assert_eq!(expandtabs("á\tb", 0), Wtf8Buf::from("áb"));
907        assert_eq!(expandtabs("\ta", 0), Wtf8Buf::from("a"));
908        assert_eq!(expandtabs("\t", 0), Wtf8Buf::from(""));
909        assert_eq!(expandtabs("", 0), Wtf8Buf::from(""));
910        assert_eq!(expandtabs("no tabs", 0), Wtf8Buf::from("no tabs"));
911    }
912
913    #[test]
914    fn expandtabs_with_a_real_tab_size_is_unchanged() {
915        assert_eq!(expandtabs("a\tb", 8), Wtf8Buf::from("a       b"));
916        assert_eq!(expandtabs("a\tb", 1), Wtf8Buf::from("a b"));
917        assert_eq!(expandtabs("abcd\te", 4), Wtf8Buf::from("abcd    e"));
918        assert_eq!(expandtabs("a\nb\tc", 4), Wtf8Buf::from("a\nb   c"));
919        assert_eq!(expandtabs("\ta", 4), Wtf8Buf::from("    a"));
920    }
921}