1use crate::diagnostics::{BuildDiagnostics, SourceLocation, Span, Spanned};
6use crate::expression_tree::WrittenUnit;
7use itertools::Itertools;
8use smol_str::SmolStr;
9use strum::IntoEnumIterator;
10
11enum EscapeChunk<'a> {
13 Plain(&'a str),
15 Escape { source_len: usize, decoded: char },
17}
18
19struct EscapeError {
22 offset: usize,
23 length: usize,
24 message: &'static str,
25}
26
27fn walk_escapes<'a>(
31 raw_token: &'a str,
32 mut callback: impl FnMut(EscapeChunk<'a>),
33) -> Result<(), EscapeError> {
34 if raw_token.contains('\n') {
35 return Err(EscapeError { offset: 0, length: 0, message: "Newline in string literal" });
36 }
37 let prefix_len = if raw_token.starts_with('"') || raw_token.starts_with('}') {
38 1
39 } else {
40 return Err(EscapeError { offset: 0, length: 0, message: "Cannot parse string literal" });
41 };
42 let content = &raw_token[prefix_len..];
43 let content = content
44 .strip_suffix('"')
45 .or_else(|| content.strip_suffix("\\{"))
46 .ok_or(EscapeError { offset: 0, length: 0, message: "Cannot parse string literal" })?;
47
48 let mut pos = 0;
49 while pos < content.len() {
50 if content.as_bytes()[pos] == b'\\' {
51 if pos + 1 >= content.len() {
52 return Err(EscapeError {
53 offset: prefix_len + pos,
54 length: 1,
55 message: r"Unknown escape sequence. Use '\\' to escape a literal backslash",
56 });
57 }
58 let (source_len, decoded) = match content.as_bytes()[pos + 1] {
59 b'"' => (2, '"'),
60 b'\\' => (2, '\\'),
61 b'n' => (2, '\n'),
62 b'u' => {
63 let brace_start = pos + 2;
64 let has_brace = content.as_bytes().get(brace_start) == Some(&b'{');
65 if !has_brace {
66 return Err(EscapeError {
67 offset: prefix_len + brace_start,
68 length: 0,
69 message: "Invalid unicode escape: expected '{'",
70 });
71 }
72 let brace_end = match content[brace_start..].find('}') {
73 Some(i) => i + brace_start,
74 None => {
75 return Err(EscapeError {
76 offset: prefix_len + brace_start,
77 length: 0,
78 message: "Unterminated unicode escape",
79 });
80 }
81 };
82 let hex = &content[brace_start + 1..brace_end];
83 let x = u32::from_str_radix(hex, 16).map_err(|_| EscapeError {
84 offset: prefix_len + brace_start + 1,
85 length: hex.len(),
86 message: "Invalid hexadecimal in unicode escape",
87 })?;
88 let ch = std::char::from_u32(x).ok_or(EscapeError {
89 offset: prefix_len + brace_start + 1,
90 length: hex.len(),
91 message: "Invalid unicode code point",
92 })?;
93 (brace_end + 1 - pos, ch)
94 }
95 _ => {
96 let next_char_len =
97 content[pos + 1..].chars().next().map_or(1, |c| c.len_utf8());
98 return Err(EscapeError {
99 offset: prefix_len + pos,
100 length: 1 + next_char_len,
101 message: r"Unknown escape sequence. Use '\\' to escape a literal backslash",
102 });
103 }
104 };
105 callback(EscapeChunk::Escape { source_len, decoded });
106 pos += source_len;
107 } else {
108 let start = pos;
109 pos = content[pos..].find('\\').map_or(content.len(), |i| pos + i);
110 callback(EscapeChunk::Plain(&content[start..pos]));
111 }
112 }
113 Ok(())
114}
115
116pub fn unescape_string(string: &str) -> Option<SmolStr> {
118 let mut result = String::with_capacity(string.len());
119 walk_escapes(string, |chunk| match chunk {
120 EscapeChunk::Plain(s) => result += s,
121 EscapeChunk::Escape { decoded, .. } => result.push(decoded),
122 })
123 .ok()?;
124 Some(result.into())
125}
126
127pub fn unescape_string_reporting(
131 token: Option<&crate::parser::SyntaxToken>,
132 diag: &mut BuildDiagnostics,
133 fallback: &dyn Spanned,
134) -> Option<SmolStr> {
135 let Some(token) = token else {
136 diag.push_error("Cannot parse string literal".into(), fallback);
137 return None;
138 };
139 let mut result = String::with_capacity(token.text().len());
140 match walk_escapes(token.text(), |chunk| match chunk {
141 EscapeChunk::Plain(s) => result += s,
142 EscapeChunk::Escape { decoded, .. } => result.push(decoded),
143 }) {
144 Ok(()) => Some(result.into()),
145 Err(e) => {
146 let loc = token.to_source_location();
147 diag.push_error_with_span(
148 e.message.into(),
149 SourceLocation {
150 source_file: loc.source_file,
151 span: Span::new(loc.span.offset + e.offset, e.length),
152 },
153 );
154 None
155 }
156 }
157}
158
159#[test]
160fn test_unescape_string() {
161 assert_eq!(unescape_string(r#""foo_bar""#).as_deref(), Some("foo_bar"));
162 assert_eq!(unescape_string(r#""foo\"bar""#).as_deref(), Some("foo\"bar"));
163 assert_eq!(unescape_string(r#""foo\\\"bar""#).as_deref(), Some("foo\\\"bar"));
164 assert_eq!(unescape_string(r#""fo\na\\r""#).as_deref(), Some("fo\na\\r"));
165 assert_eq!(unescape_string(r#""fo\xa""#), None);
166 assert_eq!(unescape_string(r#""fooo\""#), None);
167 assert_eq!(unescape_string(r#""f\n\n\nf""#).as_deref(), Some("f\n\n\nf"));
168 assert_eq!(unescape_string(r#""music\♪xx""#), None);
169 assert_eq!(unescape_string(r#""music\"♪\"🎝""#).as_deref(), Some("music\"♪\"🎝"));
170 assert_eq!(unescape_string(r#""foo_bar"#), None);
171 assert_eq!(unescape_string(r#""foo_bar\"#), None);
172 assert_eq!(unescape_string(r#"foo_bar""#), None);
173 assert_eq!(
174 unescape_string(r#""d\u{8}a\u{d4}f\u{Ed3}""#).as_deref(),
175 Some("d\u{8}a\u{d4}f\u{ED3}")
176 );
177 assert_eq!(unescape_string(r#""xxx\""#), None);
178 assert_eq!(unescape_string(r#""xxx\u""#), None);
179 assert_eq!(unescape_string(r#""xxx\uxx""#), None);
180 assert_eq!(unescape_string(r#""xxx\u{""#), None);
181 assert_eq!(unescape_string(r#""xxx\u{22""#), None);
182 assert_eq!(unescape_string(r#""xxx\u{qsdf}""#), None);
183 assert_eq!(unescape_string(r#""xxx\u{1234567890}""#), None);
184}
185
186#[derive(Default)]
189pub struct StringLiteralSourceMap {
190 assembled: String,
191 entries: Vec<SourceMapEntry>,
192}
193
194struct SourceMapEntry {
197 assembled_start: usize,
199 source_offset: usize,
201 source_file: Option<crate::diagnostics::SourceFile>,
202}
203
204impl StringLiteralSourceMap {
205 pub fn new() -> Self {
206 Self::default()
207 }
208
209 pub fn as_str(&self) -> &str {
211 &self.assembled
212 }
213
214 pub fn into_string(self) -> String {
216 self.assembled
217 }
218
219 pub fn push(
223 &mut self,
224 token: &crate::parser::SyntaxToken,
225 diag: &mut BuildDiagnostics,
226 ) -> bool {
227 let loc = token.to_source_location();
228 let token_offset = loc.span.offset;
229 let raw = token.text();
230 let base = self.assembled.len();
231
232 let mut source_pos = 1usize;
233 let mut segment_start_assembled = base;
234 let mut segment_start_source = 1usize;
235
236 let result = walk_escapes(raw, |chunk| match chunk {
237 EscapeChunk::Plain(s) => {
238 self.assembled += s;
239 source_pos += s.len();
240 }
241 EscapeChunk::Escape { source_len, decoded } => {
242 if self.assembled.len() > segment_start_assembled {
243 self.entries.push(SourceMapEntry {
244 assembled_start: segment_start_assembled,
245 source_offset: token_offset + segment_start_source,
246 source_file: loc.source_file.clone(),
247 });
248 }
249 self.entries.push(SourceMapEntry {
250 assembled_start: self.assembled.len(),
251 source_offset: token_offset + source_pos,
252 source_file: loc.source_file.clone(),
253 });
254 self.assembled.push(decoded);
255 source_pos += source_len;
256 segment_start_assembled = self.assembled.len();
257 segment_start_source = source_pos;
258 }
259 });
260
261 match result {
262 Ok(()) => {
263 if self.assembled.len() > segment_start_assembled {
264 self.entries.push(SourceMapEntry {
265 assembled_start: segment_start_assembled,
266 source_offset: token_offset + segment_start_source,
267 source_file: loc.source_file,
268 });
269 }
270 true
271 }
272 Err(e) => {
273 self.assembled.truncate(base);
274 diag.push_error_with_span(
275 e.message.into(),
276 SourceLocation {
277 source_file: loc.source_file,
278 span: Span::new(loc.span.offset + e.offset, e.length),
279 },
280 );
281 false
282 }
283 }
284 }
285
286 pub fn push_raw_char(&mut self, ch: char, loc: SourceLocation) {
289 let start = self.assembled.len();
290 self.assembled.push(ch);
291 self.entries.push(SourceMapEntry {
292 assembled_start: start,
293 source_offset: loc.span.offset,
294 source_file: loc.source_file,
295 });
296 }
297
298 pub fn resolve(&self, range: std::ops::Range<usize>) -> Option<SourceLocation> {
301 let idx = self.entries.partition_point(|e| e.assembled_start <= range.start);
302 if idx == 0 {
303 return None;
304 }
305 let entry = &self.entries[idx - 1];
306 let delta = range.start - entry.assembled_start;
307 Some(SourceLocation {
308 source_file: entry.source_file.clone(),
309 span: Span::new(entry.source_offset + delta, range.len()),
310 })
311 }
312
313 pub fn report(
316 &self,
317 diag: &mut BuildDiagnostics,
318 message: String,
319 range: std::ops::Range<usize>,
320 fallback: &dyn Spanned,
321 ) {
322 if let Some(loc) = self.resolve(range) {
323 diag.push_error_with_span(message, loc);
324 } else {
325 diag.push_error(message, fallback);
326 }
327 }
328}
329
330pub fn parse_number_literal(s: SmolStr) -> Result<(f64, WrittenUnit), SmolStr> {
331 let bytes = s.as_bytes();
332 let mut end = 0;
333 while end < bytes.len() && matches!(bytes[end], b'0'..=b'9' | b'.') {
334 end += 1;
335 }
336 let val = s[..end].parse().map_err(|_| "Cannot parse number literal".to_owned())?;
337 let unit = s[end..].parse().map_err(|_| {
338 format!(
339 "Invalid unit '{}'. Valid units are: {}",
340 s.get(end..).unwrap_or(&s),
341 WrittenUnit::iter().filter(|x| !x.to_string().is_empty()).join(", ")
342 )
343 })?;
344 Ok((val, unit))
345}
346
347#[test]
348fn test_parse_number_literal() {
349 use crate::expression_tree::WrittenUnit;
350 use smol_str::{ToSmolStr, format_smolstr};
351
352 assert_eq!(parse_number_literal("10".into()), Ok((10., WrittenUnit::None)));
353 assert_eq!(parse_number_literal("10phx".into()), Ok((10., WrittenUnit::Phx)));
354 assert_eq!(parse_number_literal("10.0phx".into()), Ok((10., WrittenUnit::Phx)));
355 assert_eq!(parse_number_literal("10.0".into()), Ok((10., WrittenUnit::None)));
356 assert_eq!(parse_number_literal("1.1phx".into()), Ok((1.1, WrittenUnit::Phx)));
357 assert_eq!(parse_number_literal("10.10".into()), Ok((10.10, WrittenUnit::None)));
358 assert_eq!(parse_number_literal("10000000".into()), Ok((10000000., WrittenUnit::None)));
359 assert_eq!(parse_number_literal("10000001phx".into()), Ok((10000001., WrittenUnit::Phx)));
360 assert_eq!(parse_number_literal("5cm".into()), Ok((5., WrittenUnit::Cm)));
361 assert_eq!(parse_number_literal("90grad".into()), Ok((90., WrittenUnit::Grad)));
362
363 let cannot_parse = Err("Cannot parse number literal".to_smolstr());
364 assert_eq!(parse_number_literal("12.10.12phx".into()), cannot_parse);
365
366 let valid_units = WrittenUnit::iter().filter(|x| !x.to_string().is_empty()).join(", ");
367 let wrong_unit_spaced =
368 Err(format_smolstr!("Invalid unit ' phx'. Valid units are: {}", valid_units));
369 assert_eq!(parse_number_literal("10000001 phx".into()), wrong_unit_spaced);
370 let wrong_unit_oo = Err(format_smolstr!("Invalid unit 'oo'. Valid units are: {}", valid_units));
371 assert_eq!(parse_number_literal("12.12oo".into()), wrong_unit_oo);
372 let wrong_unit_euro =
373 Err(format_smolstr!("Invalid unit '€'. Valid units are: {}", valid_units));
374 assert_eq!(parse_number_literal("12.12€".into()), wrong_unit_euro);
375}