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("e_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("ed), "quoted is a literal: {quoted:?}");
223 assert_eq!(unquote_text("ed), 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}