Skip to main content

praxis_syntax/
literal.rs

1//! The one literal decoder for the whole workspace (§4.3).
2//!
3//! Every caller decodes here, because a second decoder drifts from this one:
4//! one that never unescapes is how `sep("\t", int)` comes to split on the two
5//! characters `\` and `t` rather than on a tab.
6//!
7//! `praxis-syntax` depends only on `praxis-source`, so both the HIR lowerer and
8//! the input-parser's capture-body parser can reach it.
9//!
10//! [`decode_char_literal`] is here for the same rule: a `'a'` is decoded by the
11//! **lexer** (which is where its one-scalar rule is enforced, ADR-141), by
12//! [`crate::SyntaxKind::CharLit`]'s expression lowering and by its pattern
13//! lowering. Three callers, one decoder, and one escape table —
14//! [`decode_escape`] — shared with `"…"` so the two spellings of `\n` cannot
15//! drift apart.
16
17/// Whether `raw` is a well-formed text literal: at least `""`, quoted at both
18/// ends.
19#[must_use]
20pub fn is_text_literal(raw: &str) -> bool {
21    raw.len() >= 2 && raw.starts_with('"') && raw.ends_with('"')
22}
23
24/// Strip the surrounding quotes and decode the escapes of a `"…"` literal.
25///
26/// Exactly **one** quote comes off each end. An unrecognized escape is
27/// preserved verbatim (backslash and all) rather than being silently dropped —
28/// the lexer has already reported it, and rewriting it here would make the
29/// value disagree with the diagnostic.
30///
31/// A `raw` that is not a text literal is returned unchanged; callers that need
32/// to reject one should ask [`is_text_literal`] first.
33#[must_use]
34pub fn unquote_text(raw: &str) -> String {
35    if !is_text_literal(raw) {
36        return raw.to_string();
37    }
38    decode_text_body(&raw[1..raw.len() - 1])
39}
40
41/// Decode the escapes of a text literal's body — the part with the delimiters
42/// already off.
43///
44/// [`unquote_text`] is this with `"` stripped from each end. The other caller is
45/// an **interpolation fragment** (§8.1, ADR-147), whose delimiters are not both
46/// quotes: `"Part 2: {` opens with `"` and closes with `{`, and the fragments
47/// between holes open and close with braces. One byte still comes off each end,
48/// and the body is decoded by the same table — which is what keeps `"a\tb"` and
49/// `"a\tb{x}"` agreeing about what `\t` is.
50#[must_use]
51pub fn decode_text_body(inner: &str) -> String {
52    let mut out = String::with_capacity(inner.len());
53    let mut chars = inner.chars();
54    while let Some(c) = chars.next() {
55        if c == '\\' {
56            match chars.next() {
57                Some(esc) => match decode_escape(esc) {
58                    Some(decoded) => out.push(decoded),
59                    None => {
60                        out.push('\\');
61                        out.push(esc);
62                    }
63                },
64                None => out.push('\\'),
65            }
66        } else {
67            out.push(c);
68        }
69    }
70    out
71}
72
73/// The escape table both literal spellings read (§4.3).
74///
75/// `None` means "not an escape this language recognizes", and the two callers
76/// answer it differently — see [`unquote_text`] and [`decode_char_literal`] —
77/// because they have different amounts of room to preserve the mistake in. What
78/// they may not do is disagree about what `\n` *is*, which is why the eight rows
79/// live here rather than in each of them.
80///
81/// `\{` and `\}` are in the table because of §8.1's interpolation (ADR-147): a
82/// `{` in a text literal opens a hole, so a literal brace needs a spelling. It
83/// is an escape rather than a doubling rule (`{{`) because the language has one
84/// escape table and every other literal brace-free character already goes
85/// through it — a doubling rule would be a second mechanism that only
86/// interpolation used. `\}` is accepted so a pair can be written symmetrically;
87/// it is not *required*, since outside a hole a `}` closes nothing.
88#[must_use]
89pub fn decode_escape(esc: char) -> Option<char> {
90    match esc {
91        'n' => Some('\n'),
92        't' => Some('\t'),
93        'r' => Some('\r'),
94        '"' => Some('"'),
95        '\\' => Some('\\'),
96        '0' => Some('\0'),
97        '{' => Some('{'),
98        '}' => Some('}'),
99        _ => None,
100    }
101}
102
103/// Write `s` as a quoted, escaped text literal — the direction
104/// [`decode_text_body`] does not go.
105///
106/// The debugger renders values, and a `Text` rendered as itself is ambiguous in
107/// three ways it cannot afford: `""` writes zero bytes and is indistinguishable
108/// from a failed read, a value containing `"` cannot be told from two values,
109/// and a value containing a newline takes a second line on a display that gives
110/// each value one. Quoting answers all three, and the escaping is what keeps the
111/// quoting honest.
112///
113/// ### What round-trips, and what does not
114///
115/// `decode_text_body(&quote_text(s)[1..len-1]) == s` for every `s` — that is the
116/// property, and [`quoting_round_trips_through_the_decoder`] is it as a test.
117///
118/// It is deliberately *not* "the output re-lexes as a literal spelling `s`".
119/// `{` opens an interpolation hole in source (§8.1) and is left unescaped here,
120/// because this text is read by a person looking at a locals pane and not by the
121/// lexer, and `{"a": 1}` is worth more on that pane than `\{"a": 1\}`. The
122/// round-trip above still holds through it: `decode_escape` only ever looks at
123/// the character *after* a backslash, so an unescaped brace decodes to itself.
124///
125/// [`quoting_round_trips_through_the_decoder`]: #
126#[must_use]
127pub fn quote_text(s: &str) -> String {
128    let mut out = String::with_capacity(s.len() + 2);
129    out.push('"');
130    for c in s.chars() {
131        match c {
132            // The two that would make the quoting a lie, and the three that
133            // would make the value take more than its line.
134            '\\' => out.push_str("\\\\"),
135            '"' => out.push_str("\\\""),
136            '\n' => out.push_str("\\n"),
137            '\t' => out.push_str("\\t"),
138            '\r' => out.push_str("\\r"),
139            '\0' => out.push_str("\\0"),
140            _ => out.push(c),
141        }
142    }
143    out.push('"');
144    out
145}
146
147/// Why a `'…'` run is not a character literal.
148///
149/// Three variants rather than one, because the lexer's message is what makes the
150/// difference between the three legible: `''` names no character, `'ab'` names
151/// two, and `'a` never closed. Collapsing them would report the length rule for
152/// a literal whose real problem is a missing quote.
153#[derive(Clone, Copy, PartialEq, Eq, Debug)]
154pub enum CharLitError {
155    /// No closing `'` — the token ran to the end of its line or the file.
156    Unterminated,
157    /// `''`: the body is empty, so there is no character to name.
158    Empty,
159    /// `'ab'`: the body decodes to more than one Unicode scalar.
160    ///
161    /// This is the variant the feature exists for. `"ab"[0]` is a well-typed
162    /// program that silently means `a`; `'ab'` is a lexical error.
163    TooLong,
164}
165
166/// Whether `raw` is a well-formed character literal: at least `''`, quoted at
167/// both ends.
168#[must_use]
169pub fn is_char_literal(raw: &str) -> bool {
170    raw.len() >= 2 && raw.starts_with('\'') && raw.ends_with('\'')
171}
172
173/// Strip the surrounding quotes and decode the escapes of a `'…'` literal,
174/// answering the **one** Unicode scalar it names (ADR-141).
175///
176/// One quote comes off each end and the body must decode to exactly one scalar;
177/// anything else is a [`CharLitError`], which is the whole of the one-character
178/// rule and the reason the lexer asks this function rather than counting bytes.
179/// `'é'` is one character, not two.
180///
181/// An **unrecognized** escape decodes to the escaped scalar itself (`'\q'` is
182/// `q`), where [`unquote_text`] preserves the backslash verbatim. The two differ
183/// deliberately: the lexer has already reported `T005` either way, and a char
184/// literal that preserved `\q` would then have two scalars in it and earn a
185/// second, spurious "not one character" on top of the report the author already
186/// has.
187pub fn decode_char_literal(raw: &str) -> Result<char, CharLitError> {
188    if !is_char_literal(raw) {
189        return Err(CharLitError::Unterminated);
190    }
191    let inner = &raw[1..raw.len() - 1];
192    let mut chars = inner.chars();
193    let decoded = match chars.next() {
194        None => return Err(CharLitError::Empty),
195        Some('\\') => match chars.next() {
196            // `'\` at the end of the body: the closing quote was eaten by the
197            // escape, so the literal never closed.
198            None => return Err(CharLitError::Unterminated),
199            Some(esc) => decode_escape(esc).unwrap_or(esc),
200        },
201        Some(c) => c,
202    };
203    if chars.next().is_some() {
204        return Err(CharLitError::TooLong);
205    }
206    Ok(decoded)
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    /// Every string survives quoting and decoding back, which is what makes
214    /// [`quote_text`] an inverse of the table rather than a second opinion about
215    /// it.
216    #[test]
217    fn quoting_round_trips_through_the_decoder() {
218        for s in [
219            "", "asdf", "\"", "\\", "a\"b\\c", "\n\t\r\0", "{x}", "a\\nb", "héllo",
220        ] {
221            let quoted = quote_text(s);
222            assert!(is_text_literal(&quoted), "quoted is a literal: {quoted:?}");
223            assert_eq!(unquote_text(&quoted), s, "round trip of {s:?}");
224        }
225    }
226
227    /// The visible half: an empty `Text` becomes two characters instead of
228    /// nothing at all, which is the whole reason the debugger quotes.
229    #[test]
230    fn an_empty_text_still_renders_as_something() {
231        assert_eq!(quote_text(""), "\"\"");
232        assert_eq!(quote_text("a\nb"), r#""a\nb""#, "and stays on one line");
233        // A brace is left alone: this is a rendering for a person, not a
234        // spelling for the lexer — see `quote_text`'s note.
235        assert_eq!(quote_text("{x}"), r#""{x}""#);
236    }
237
238    #[test]
239    fn one_quote_comes_off_each_end() {
240        assert_eq!(unquote_text(r#""a""#), "a");
241        assert_eq!(unquote_text(r#""""#), "");
242        // A literal whose content is two escaped quotes keeps both.
243        assert_eq!(unquote_text(r#""\"\"""#), "\"\"");
244    }
245
246    #[test]
247    fn the_six_escapes_decode() {
248        assert_eq!(unquote_text(r#""\n\t\r\"\\\0""#), "\n\t\r\"\\\0");
249    }
250
251    #[test]
252    fn an_unknown_escape_is_preserved_verbatim() {
253        assert_eq!(unquote_text(r#""\q""#), r"\q");
254        assert_eq!(unquote_text(r#""a\""#), r#"a\"#);
255    }
256
257    #[test]
258    fn a_non_literal_is_not_a_literal() {
259        assert!(!is_text_literal("a"));
260        assert!(!is_text_literal("\""));
261        assert!(!is_text_literal("\"a"));
262        assert!(is_text_literal("\"\""));
263    }
264
265    // --- the character literal (ADR-141) ---
266
267    #[test]
268    fn one_quote_comes_off_each_end_of_a_char() {
269        assert_eq!(decode_char_literal("'a'"), Ok('a'));
270        assert_eq!(decode_char_literal("'#'"), Ok('#'));
271        assert_eq!(decode_char_literal("'\"'"), Ok('"'));
272    }
273
274    #[test]
275    fn a_char_takes_texts_escapes_plus_the_quote() {
276        assert_eq!(decode_char_literal(r"'\n'"), Ok('\n'));
277        assert_eq!(decode_char_literal(r"'\t'"), Ok('\t'));
278        assert_eq!(decode_char_literal(r"'\r'"), Ok('\r'));
279        assert_eq!(decode_char_literal(r"'\0'"), Ok('\0'));
280        assert_eq!(decode_char_literal(r"'\\'"), Ok('\\'));
281        assert_eq!(decode_char_literal(r#"'\"'"#), Ok('"'));
282        // The one escape a `"…"` does not need and a `'…'` does.
283        assert_eq!(decode_char_literal(r"'\''"), Ok('\''));
284    }
285
286    /// A multi-byte scalar is **one** character. An implementation that counted
287    /// bytes would call `'é'` two and refuse it.
288    #[test]
289    fn a_multibyte_scalar_is_one_character() {
290        assert_eq!(decode_char_literal("'é'"), Ok('é'));
291        assert_eq!(decode_char_literal("'😀'"), Ok('😀'));
292        assert_eq!(decode_char_literal("'字'"), Ok('字'));
293    }
294
295    /// The three ways a `'…'` fails to name exactly one character, each caught
296    /// at lex time rather than at run time or not at all (ADR-141 Decision 2).
297    #[test]
298    fn a_char_literal_names_exactly_one_character() {
299        assert_eq!(decode_char_literal("''"), Err(CharLitError::Empty));
300        assert_eq!(decode_char_literal("'ab'"), Err(CharLitError::TooLong));
301        assert_eq!(decode_char_literal("'éé'"), Err(CharLitError::TooLong));
302        assert_eq!(decode_char_literal(r"'\na'"), Err(CharLitError::TooLong));
303        assert_eq!(decode_char_literal("'a"), Err(CharLitError::Unterminated));
304        assert_eq!(decode_char_literal("'"), Err(CharLitError::Unterminated));
305        // `'\` — the escape ate the closing quote, so nothing closed it.
306        assert_eq!(decode_char_literal(r"'\'"), Err(CharLitError::Unterminated));
307    }
308
309    /// An unknown escape answers the escaped scalar, where a `"…"` keeps the
310    /// backslash. Both are already reported as `T005`; this is the difference
311    /// between one diagnostic and two.
312    #[test]
313    fn an_unknown_char_escape_is_the_escaped_scalar() {
314        assert_eq!(decode_char_literal(r"'\q'"), Ok('q'));
315        assert_eq!(unquote_text(r#""\q""#), r"\q");
316    }
317
318    #[test]
319    fn a_non_char_literal_is_not_a_char_literal() {
320        assert!(!is_char_literal("a"));
321        assert!(!is_char_literal("'"));
322        assert!(!is_char_literal("'a"));
323        assert!(is_char_literal("''"));
324    }
325}