Skip to main content

kohebi_core/
text.rs

1//! Python strings, and how `repr` prints them and bytes.
2//!
3//! A Python string is a sequence of code points rather than of characters, so
4//! it can hold a lone surrogate, which `'\ud800'` produces and which a Rust
5//! `str` cannot represent. [`Str`] is the two cases that fact forces, and the
6//! common one costs nothing.
7//!
8//! `repr` is here rather than in the parser because two things need it and
9//! need to agree. `ast.dump` prints every constant and every identifier with
10//! `repr`, and it is compared character for character against CPython in
11//! `tamnd/kohebi-compat`. The runtime needs the same function for the `repr`
12//! builtin. One of them being subtly different from the other would be a bug
13//! nobody could see from either side.
14
15use std::fmt::Write as _;
16
17use crate::printable::is_printable;
18
19/// A Python string, which is a sequence of code points.
20///
21/// Nearly every string in nearly every program is valid UTF-8 and takes the
22/// first arm, which is a `Box<str>` and costs nothing. `'\ud800'` is a lone
23/// surrogate, which is a perfectly ordinary Python string and something a Rust
24/// `str` cannot hold, so a string containing one takes the second arm and
25/// spends four bytes a code point. Paying that for every string to serve the
26/// few that need it would be the wrong trade, and refusing them, which is what
27/// this did until now, is worse: 58 files in CPython's own standard library
28/// have one.
29///
30/// The two arms are never both valid for the same string. A `Wide` is built
31/// only after a surrogate has arrived, so `Utf8` and `Wide` never hold the same
32/// sequence and `PartialEq` can stay derived.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum Str {
35    /// The usual case.
36    Utf8(Box<str>),
37    /// Code points, for a string holding at least one lone surrogate.
38    Wide(Box<[u32]>),
39}
40
41impl Str {
42    /// The code points, in order, whatever the string is stored as.
43    pub fn code_points(&self) -> impl Iterator<Item = u32> + '_ {
44        // Two shapes, one iterator, so callers never have to know which arm
45        // they were handed.
46        let (text, wide) = match self {
47            Str::Utf8(s) => (Some(s.chars()), None),
48            Str::Wide(w) => (None, Some(w.iter().copied())),
49        };
50        text.into_iter()
51            .flatten()
52            .map(u32::from)
53            .chain(wide.into_iter().flatten())
54    }
55
56    /// What CPython's `repr` prints for this string.
57    #[must_use]
58    pub fn repr(&self) -> String {
59        match self {
60            Str::Utf8(s) => str_repr(s),
61            Str::Wide(w) => repr_code_points(w.iter().copied(), w.len()),
62        }
63    }
64
65    /// How many code points, which is what `len` answers for a `str`.
66    ///
67    /// Linear for a `Utf8` string, since UTF-8 does not carry a count. CPython
68    /// stores one in the object header and answers in constant time, and the
69    /// representation that will do the same here is the one in the spec rather
70    /// than this one.
71    #[must_use]
72    pub fn len(&self) -> usize {
73        match self {
74            // Nearly every string is ASCII, and for those a byte is a code
75            // point, so the scan `is_ascii` does is the whole cost.
76            Str::Utf8(s) if s.is_ascii() => s.len(),
77            Str::Utf8(s) => s.chars().count(),
78            Str::Wide(w) => w.len(),
79        }
80    }
81
82    /// The code point at an offset, or `None` past the end.
83    ///
84    /// Linear in the offset for a string that is not ASCII, because UTF-8 has
85    /// no way to reach the nth code point except by counting to it. Anything
86    /// walking a whole string wants [`Str::code_points`] instead, which counts
87    /// once.
88    #[must_use]
89    pub fn code_point_at(&self, index: usize) -> Option<u32> {
90        match self {
91            Str::Utf8(s) if s.is_ascii() => s.as_bytes().get(index).copied().map(u32::from),
92            Str::Utf8(s) => s.chars().nth(index).map(u32::from),
93            Str::Wide(w) => w.get(index).copied(),
94        }
95    }
96
97    /// Whether this is the empty string, which decides `Str` vs `JoinedStr`.
98    #[must_use]
99    pub fn is_empty(&self) -> bool {
100        match self {
101            Str::Utf8(s) => s.is_empty(),
102            Str::Wide(w) => w.is_empty(),
103        }
104    }
105}
106
107impl std::fmt::Display for Str {
108    /// The text itself, which is what `str` gives back and what `print` writes.
109    ///
110    /// A lone surrogate has no UTF-8 encoding, and CPython raises
111    /// `UnicodeEncodeError` rather than writing one. Until there is an encoder
112    /// to raise it from, one is written as the replacement character, which is
113    /// what every other tool that has to keep going does.
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        match self {
116            Str::Utf8(s) => f.write_str(s),
117            Str::Wide(w) => w
118                .iter()
119                .map(|&cp| char::from_u32(cp).unwrap_or(char::REPLACEMENT_CHARACTER))
120                .try_for_each(|c| f.write_char(c)),
121        }
122    }
123}
124
125impl From<&str> for Str {
126    fn from(s: &str) -> Self {
127        Str::Utf8(s.into())
128    }
129}
130
131impl From<String> for Str {
132    fn from(s: String) -> Self {
133        Str::Utf8(s.into_boxed_str())
134    }
135}
136
137/// Builds a Python string, staying on the cheap path until it cannot.
138///
139/// Every literal goes through this, and adjacent literals are concatenated
140/// into one, so the surrogate case has to be reachable from anywhere in a
141/// string rather than only at the start. Once one arrives the buffer widens
142/// once and never narrows again, because narrowing would mean checking on
143/// every push for a case that has already happened.
144#[derive(Debug, Default)]
145pub struct StrBuf {
146    text: String,
147    /// Set the moment a lone surrogate arrives. `text` is spent then.
148    wide: Option<Vec<u32>>,
149}
150
151impl StrBuf {
152    #[must_use]
153    pub fn new() -> Self {
154        Self::default()
155    }
156
157    pub fn push(&mut self, c: char) {
158        match &mut self.wide {
159            Some(wide) => wide.push(u32::from(c)),
160            None => self.text.push(c),
161        }
162    }
163
164    pub fn push_str(&mut self, s: &str) {
165        match &mut self.wide {
166            Some(wide) => wide.extend(s.chars().map(u32::from)),
167            None => self.text.push_str(s),
168        }
169    }
170
171    /// Append one code point, which may be a lone surrogate.
172    ///
173    /// This is the only way into the wide representation, and the caller has
174    /// already decided the value is a code point rather than a scalar value.
175    pub fn push_code_point(&mut self, cp: u32) {
176        if let Some(c) = char::from_u32(cp) {
177            self.push(c);
178            return;
179        }
180        self.widen().push(cp);
181    }
182
183    /// Append everything in another string, whichever arm it is in.
184    pub fn push_string(&mut self, other: &Str) {
185        match other {
186            Str::Utf8(s) => self.push_str(s),
187            Str::Wide(w) => {
188                let wide = self.widen();
189                wide.extend(w.iter().copied());
190            }
191        }
192    }
193
194    fn widen(&mut self) -> &mut Vec<u32> {
195        self.wide.get_or_insert_with(|| {
196            let mut wide: Vec<u32> = Vec::with_capacity(self.text.len() + 1);
197            wide.extend(self.text.chars().map(u32::from));
198            self.text = String::new();
199            wide
200        })
201    }
202
203    #[must_use]
204    pub fn is_empty(&self) -> bool {
205        match &self.wide {
206            Some(wide) => wide.is_empty(),
207            None => self.text.is_empty(),
208        }
209    }
210
211    /// Empty the buffer and go back to the narrow representation.
212    ///
213    /// A buffer that widened once did so because of one literal, and the next
214    /// run it collects has no reason to inherit that.
215    pub fn clear(&mut self) {
216        self.text.clear();
217        self.wide = None;
218    }
219
220    #[must_use]
221    pub fn finish(self) -> Str {
222        match self.wide {
223            Some(wide) => Str::Wide(wide.into_boxed_slice()),
224            None => Str::Utf8(self.text.into_boxed_str()),
225        }
226    }
227}
228
229/// `repr` of a string, quote choice and escapes included.
230///
231/// Public because `ast.dump` prints identifiers with `repr` too, so `name='f'`
232/// and `alias(name='a.b')` go through exactly this function.
233#[must_use]
234pub fn str_repr(s: &str) -> String {
235    repr_code_points(s.chars().map(u32::from), s.len())
236}
237
238/// `repr` of a sequence of code points, which is what a Python string is.
239///
240/// Taking code points rather than characters is what lets a lone surrogate
241/// through. There is no `char` for one and there is no printable character
242/// either, since a surrogate is category `Cs`, so it takes the escape arm and
243/// prints as `\ud800` exactly as CPython prints it.
244///
245/// `hint` is the byte length if one is known, and only sizes the buffer.
246#[must_use]
247pub fn repr_code_points(code_points: impl Iterator<Item = u32> + Clone, hint: usize) -> String {
248    // A string with an apostrophe in it and no double quote is printed in
249    // double quotes, so that the apostrophe does not need escaping. That needs
250    // to be known before the first character is written, hence the extra pass.
251    let mut has_single = false;
252    let mut has_double = false;
253    for cp in code_points.clone() {
254        has_single |= cp == u32::from('\'');
255        has_double |= cp == u32::from('"');
256    }
257    let quote = if has_single && !has_double { '"' } else { '\'' };
258
259    let mut out = String::with_capacity(hint + 2);
260    out.push(quote);
261    for cp in code_points {
262        match char::from_u32(cp) {
263            Some('\\') => out.push_str("\\\\"),
264            Some('\t') => out.push_str("\\t"),
265            Some('\n') => out.push_str("\\n"),
266            Some('\r') => out.push_str("\\r"),
267            Some(c) if c == quote => {
268                out.push('\\');
269                out.push(c);
270            }
271            Some(c) if is_printable(c) => out.push(c),
272            // Everything else, and every surrogate, which has no `char`.
273            _ => push_escape(&mut out, cp),
274        }
275    }
276    out.push(quote);
277    out
278}
279
280/// `repr` of a bytes object, which is the same shape with different rules.
281///
282/// Everything outside printable ASCII is `\xNN`, since there is no character
283/// there to print. The quote choice is the same as a string's.
284#[must_use]
285pub fn bytes_repr(b: &[u8]) -> String {
286    let quote = if b.contains(&b'\'') && !b.contains(&b'"') {
287        b'"'
288    } else {
289        b'\''
290    };
291    let mut out = String::with_capacity(b.len() + 3);
292    out.push('b');
293    out.push(quote as char);
294    for &byte in b {
295        match byte {
296            b'\\' => out.push_str("\\\\"),
297            b'\t' => out.push_str("\\t"),
298            b'\n' => out.push_str("\\n"),
299            b'\r' => out.push_str("\\r"),
300            b if b == quote => {
301                out.push('\\');
302                out.push(b as char);
303            }
304            0x20..=0x7E => out.push(byte as char),
305            b => {
306                let _ = write!(out, "\\x{b:02x}");
307            }
308        }
309    }
310    out.push(quote as char);
311    out
312}
313
314/// The `\x`, `\u`, or `\U` form for a code point, chosen by how wide it is.
315fn push_escape(out: &mut String, cp: u32) {
316    let _ = if cp < 0x100 {
317        write!(out, "\\x{cp:02x}")
318    } else if cp < 0x1_0000 {
319        write!(out, "\\u{cp:04x}")
320    } else {
321        write!(out, "\\U{cp:08x}")
322    };
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328
329    #[test]
330    fn a_string_with_an_apostrophe_changes_quotes_rather_than_escaping() {
331        assert_eq!(str_repr("it's"), "\"it's\"");
332        assert_eq!(str_repr("it's \"so\""), "'it\\'s \"so\"'");
333        assert_eq!(str_repr("\"quoted\""), "'\"quoted\"'");
334    }
335
336    #[test]
337    fn control_characters_are_escaped_and_printable_ones_are_not() {
338        assert_eq!(str_repr("a\tb\nc\rd\\e"), "'a\\tb\\nc\\rd\\\\e'");
339        assert_eq!(str_repr("\x00\x1b\x7f"), "'\\x00\\x1b\\x7f'");
340        assert_eq!(str_repr("héllo"), "'héllo'");
341        assert_eq!(str_repr("\u{200b}"), "'\\u200b'");
342        assert_eq!(str_repr("\u{e0001}"), "'\\U000e0001'");
343    }
344
345    #[test]
346    fn bytes_print_everything_outside_printable_ascii_as_hex() {
347        assert_eq!(bytes_repr(b"abc"), "b'abc'");
348        assert_eq!(bytes_repr(&[0, 0x7f, 0xff]), "b'\\x00\\x7f\\xff'");
349        assert_eq!(bytes_repr(b"it's"), "b\"it's\"");
350    }
351
352    /// A surrogate is category `Cs`, so it is unprintable and takes the escape
353    /// arm, which prints it the way CPython prints it.
354    #[test]
355    fn a_lone_surrogate_prints_as_the_escape_that_made_it() {
356        let mut out = StrBuf::new();
357        out.push_code_point(0xD800);
358        assert_eq!(out.finish().repr(), "'\\ud800'");
359    }
360
361    /// `repr` quotes and escapes, `Display` gives the text back as it is.
362    #[test]
363    fn displaying_a_string_writes_the_text_and_not_the_quotes() {
364        assert_eq!(Str::from("it's").to_string(), "it's");
365        assert_eq!(Str::from("a\tb").to_string(), "a\tb");
366
367        let mut out = StrBuf::new();
368        out.push_str("a");
369        out.push_code_point(0xD800);
370        out.push_str("b");
371        // The surrogate has no encoding, so it comes out as the replacement
372        // character rather than stopping the two ordinary letters around it.
373        assert_eq!(out.finish().to_string(), "a\u{fffd}b");
374    }
375
376    /// Two escapes that look like a surrogate pair are two code points in
377    /// Python and do not combine into the character they would encode in
378    /// UTF-16, so joining them is not something the buffer may do.
379    #[test]
380    fn what_looks_like_a_surrogate_pair_stays_two_code_points() {
381        let mut out = StrBuf::new();
382        out.push_code_point(0xD83D);
383        out.push_code_point(0xDE00);
384        let value = out.finish();
385        assert_eq!(value.code_points().count(), 2);
386        assert_eq!(value.repr(), "'\\ud83d\\ude00'");
387    }
388
389    /// The quote is chosen over the whole string, so the code point path has
390    /// to reach the same answer the character path reaches.
391    #[test]
392    fn the_quote_choice_survives_widening() {
393        let mut out = StrBuf::new();
394        out.push_str("it's ");
395        out.push_code_point(0xD800);
396        assert_eq!(out.finish().repr(), "\"it's \\ud800\"");
397    }
398
399    /// Text written before the surrogate arrived has to come out in front of
400    /// it, which is the one thing widening in the middle could get wrong.
401    #[test]
402    fn widening_keeps_what_was_already_in_the_buffer() {
403        let mut out = StrBuf::new();
404        out.push_str("héllo ");
405        out.push_code_point(0xDFFF);
406        out.push('!');
407        assert_eq!(out.finish().repr(), "'héllo \\udfff!'");
408    }
409
410    /// A buffer nothing widened stays narrow, which is the whole point of the
411    /// two arms and is not visible from the repr.
412    #[test]
413    fn the_common_case_never_leaves_the_narrow_arm() {
414        let mut out = StrBuf::new();
415        out.push_str("plain");
416        out.push_code_point(0x1F600);
417        assert!(matches!(out.finish(), Str::Utf8(_)));
418    }
419
420    #[test]
421    fn clearing_a_widened_buffer_goes_back_to_narrow() {
422        let mut out = StrBuf::new();
423        out.push_code_point(0xD800);
424        assert!(!out.is_empty());
425        out.clear();
426        assert!(out.is_empty());
427        out.push_str("after");
428        assert_eq!(out.finish(), Str::Utf8("after".into()));
429    }
430
431    #[test]
432    fn joining_two_strings_widens_only_when_one_of_them_is_wide() {
433        let mut wide = StrBuf::new();
434        wide.push_code_point(0xD800);
435        let wide = wide.finish();
436
437        let mut out = StrBuf::new();
438        out.push_string(&Str::from("a"));
439        out.push_string(&wide);
440        out.push_string(&Str::from("b"));
441        assert_eq!(out.finish().repr(), "'a\\ud800b'");
442    }
443}