1use std::{num::IntErrorKind, ops};
4
5use squawk_lexer::tokenize;
6
7use crate::SyntaxKind;
8
9pub struct LexedStr<'a> {
10 text: &'a str,
11 kind: Vec<SyntaxKind>,
12 start: Vec<u32>,
13 error: Vec<LexError>,
14}
15
16struct LexError {
17 msg: String,
18 range: ops::Range<u32>,
19}
20
21impl<'a> LexedStr<'a> {
22 pub fn new(text: &'a str) -> LexedStr<'a> {
25 let mut conv = Converter::new(text);
26
27 for token in tokenize(&text[conv.offset..]) {
28 let token_text = &text[conv.offset..][..token.len as usize];
29
30 conv.extend_token(&token.kind, token_text);
31 }
32
33 conv.finalize_with_eof()
34 }
35
36 pub(crate) fn len(&self) -> usize {
59 self.kind.len() - 1
60 }
61
62 pub(crate) fn kind(&self, i: usize) -> SyntaxKind {
67 assert!(i < self.len());
68 self.kind[i]
69 }
70
71 pub(crate) fn text(&self, i: usize) -> &str {
72 self.range_text(i..i + 1)
73 }
74
75 pub(crate) fn range_text(&self, r: ops::Range<usize>) -> &str {
76 assert!(r.start < r.end && r.end <= self.len());
77 let lo = self.start[r.start] as usize;
78 let hi = self.start[r.end] as usize;
79 &self.text[lo..hi]
80 }
81
82 pub fn text_range(&self, i: usize) -> ops::Range<usize> {
84 assert!(i < self.len());
85 let lo = self.start[i] as usize;
86 let hi = self.start[i + 1] as usize;
87 lo..hi
88 }
89 pub fn text_start(&self, i: usize) -> usize {
90 assert!(i <= self.len());
91 self.start[i] as usize
92 }
93 pub fn errors(&self) -> impl Iterator<Item = (&ops::Range<u32>, &str)> + '_ {
109 self.error.iter().map(|it| (&it.range, it.msg.as_str()))
110 }
111
112 fn push(&mut self, kind: SyntaxKind, offset: usize) {
113 self.kind.push(kind);
114 self.start.push(offset as u32);
115 }
116}
117
118struct Converter<'a> {
119 res: LexedStr<'a>,
120 offset: usize,
121 prefixed_string_continuation: Option<PrefixedStringKind>,
122}
123
124#[derive(Clone, Copy)]
125enum PrefixedStringKind {
126 Bit,
127 Byte,
128 Escape,
129}
130
131fn is_empty_quoted_ident(token_text: &str, uescape: bool) -> bool {
132 let inner = if uescape {
133 token_text
134 .strip_prefix(['u', 'U'])
135 .and_then(|s| s.strip_prefix('&'))
136 } else {
137 Some(token_text)
138 };
139 inner == Some("\"\"")
140}
141
142impl<'a> Converter<'a> {
143 fn new(text: &'a str) -> Self {
144 Self {
145 res: LexedStr {
146 text,
147 kind: Vec::new(),
148 start: Vec::new(),
149 error: Vec::new(),
150 },
151 offset: 0,
152 prefixed_string_continuation: None,
153 }
154 }
155
156 fn finalize_with_eof(mut self) -> LexedStr<'a> {
157 self.res.push(SyntaxKind::EOF, self.offset);
158 self.res
159 }
160
161 fn push(&mut self, kind: SyntaxKind, len: usize, err: Option<(&str, ops::Range<u32>)>) {
162 let token_start = self.offset as u32;
163 self.res.push(kind, self.offset);
164 self.offset += len;
165
166 if let Some((msg, err_range)) = err {
167 self.res.error.push(LexError {
168 msg: msg.to_owned(),
169 range: token_start + err_range.start..token_start + err_range.end,
170 });
171 }
172 }
173
174 fn extend_token(&mut self, kind: &squawk_lexer::TokenKind, token_text: &str) {
175 if !matches!(
176 kind,
177 squawk_lexer::TokenKind::Whitespace
178 | squawk_lexer::TokenKind::LineComment
179 | squawk_lexer::TokenKind::BlockComment { .. }
180 | squawk_lexer::TokenKind::Literal { .. }
181 ) {
182 self.prefixed_string_continuation = None;
183 }
184
185 let mut err = "";
190 let mut err_range: Option<ops::Range<u32>> = None;
191
192 let syntax_kind = {
193 match kind {
194 squawk_lexer::TokenKind::LineComment => SyntaxKind::COMMENT,
195 squawk_lexer::TokenKind::BlockComment { terminated } => {
196 if !terminated {
197 err = "Missing trailing `*/` symbols to terminate the block comment";
198 }
199 SyntaxKind::COMMENT
200 }
201
202 squawk_lexer::TokenKind::Whitespace => SyntaxKind::WHITESPACE,
203 squawk_lexer::TokenKind::Ident => {
204 SyntaxKind::from_keyword(token_text).unwrap_or(SyntaxKind::IDENT)
205 }
206 squawk_lexer::TokenKind::Literal { kind, .. } => {
207 self.extend_literal(token_text, kind);
208 return;
209 }
210 squawk_lexer::TokenKind::Semi => SyntaxKind::SEMICOLON,
211 squawk_lexer::TokenKind::Comma => SyntaxKind::COMMA,
212 squawk_lexer::TokenKind::Dot => SyntaxKind::DOT,
213 squawk_lexer::TokenKind::DotDot => SyntaxKind::DOT_DOT,
214 squawk_lexer::TokenKind::OpenParen => SyntaxKind::L_PAREN,
215 squawk_lexer::TokenKind::CloseParen => SyntaxKind::R_PAREN,
216 squawk_lexer::TokenKind::OpenBracket => SyntaxKind::L_BRACK,
217 squawk_lexer::TokenKind::CloseBracket => SyntaxKind::R_BRACK,
218 squawk_lexer::TokenKind::OpenCurly => SyntaxKind::L_CURLY,
219 squawk_lexer::TokenKind::CloseCurly => SyntaxKind::R_CURLY,
220 squawk_lexer::TokenKind::At => SyntaxKind::AT,
221 squawk_lexer::TokenKind::Pound => SyntaxKind::POUND,
222 squawk_lexer::TokenKind::Tilde => SyntaxKind::TILDE,
223 squawk_lexer::TokenKind::Question => SyntaxKind::QUESTION,
224 squawk_lexer::TokenKind::Colon => SyntaxKind::COLON,
225 squawk_lexer::TokenKind::Eq => SyntaxKind::EQ,
226 squawk_lexer::TokenKind::Bang => SyntaxKind::BANG,
227 squawk_lexer::TokenKind::Lt => SyntaxKind::L_ANGLE,
228 squawk_lexer::TokenKind::Gt => SyntaxKind::R_ANGLE,
229 squawk_lexer::TokenKind::Minus => SyntaxKind::MINUS,
230 squawk_lexer::TokenKind::And => SyntaxKind::AMP,
231 squawk_lexer::TokenKind::Or => SyntaxKind::PIPE,
232 squawk_lexer::TokenKind::Plus => SyntaxKind::PLUS,
233 squawk_lexer::TokenKind::Star => SyntaxKind::STAR,
234 squawk_lexer::TokenKind::Slash => SyntaxKind::SLASH,
235 squawk_lexer::TokenKind::Caret => SyntaxKind::CARET,
236 squawk_lexer::TokenKind::Percent => SyntaxKind::PERCENT,
237 squawk_lexer::TokenKind::Unknown => SyntaxKind::ERROR,
238 squawk_lexer::TokenKind::Eof => SyntaxKind::EOF,
239 squawk_lexer::TokenKind::Backtick => SyntaxKind::BACKTICK,
240 squawk_lexer::TokenKind::PositionalParam {
241 trailing_junk_start,
242 } => {
243 let digits = &token_text[1..*trailing_junk_start as usize];
244 if digits.is_empty() {
245 err = "missing parameter number";
246 err_range = Some(0..1);
247 } else if digits
248 .parse::<i32>()
249 .is_err_and(|err| matches!(err.kind(), IntErrorKind::PosOverflow))
250 {
251 err = "parameter number too large";
252 err_range = Some(0..*trailing_junk_start);
253 } else if (*trailing_junk_start as usize) < token_text.len() {
254 err = "trailing junk after positional parameter";
255 err_range = Some(*trailing_junk_start..token_text.len() as u32);
256 }
257 SyntaxKind::POSITIONAL_PARAM
258 }
259 squawk_lexer::TokenKind::QuotedIdent {
260 terminated,
261 uescape,
262 } => {
263 if !terminated {
264 err = "Missing trailing \" to terminate the quoted identifier"
265 } else if is_empty_quoted_ident(token_text, *uescape) {
266 err = "empty delimited identifier";
267 }
268 SyntaxKind::IDENT
269 }
270 }
271 };
272
273 let err = if err.is_empty() { None } else { Some(err) };
274 let err = err.map(|msg| (msg, err_range.unwrap_or(0..token_text.len() as u32)));
275 self.push(syntax_kind, token_text.len(), err);
276 }
277
278 fn extend_literal(&mut self, token_text: &str, kind: &squawk_lexer::LiteralKind) {
279 let mut err: Option<String> = None;
280 let mut err_range: Option<ops::Range<u32>> = None;
281 let continuation = self.prefixed_string_continuation.take();
282
283 let syntax_kind = match *kind {
284 squawk_lexer::LiteralKind::Int {
285 empty_int,
286 base,
287 trailing_junk_start,
288 } => {
289 if empty_int {
290 err = Some("Missing digits after the integer base prefix".into());
291 } else {
292 if matches!(base, squawk_lexer::Base::Binary | squawk_lexer::Base::Octal) {
293 let prefix_len = 2u32;
294 let digits = &token_text[prefix_len as usize..trailing_junk_start as usize];
295 let base = base as u32;
296 let token_start = self.offset as u32;
297 for (i, c) in digits.char_indices() {
298 if c != '_' && c.to_digit(base).is_none() {
299 let start = token_start + prefix_len + i as u32;
300 let end = start + c.len_utf8() as u32;
301 self.res.error.push(LexError {
302 msg: format!("invalid digit for a base {base} literal"),
303 range: start..end,
304 });
305 }
306 }
307 }
308 if (trailing_junk_start as usize) < token_text.len() {
309 err = Some("trailing junk after numeric literal".into());
310 err_range = Some(trailing_junk_start..token_text.len() as u32);
311 }
312 }
313 SyntaxKind::INT_NUMBER
314 }
315 squawk_lexer::LiteralKind::Numeric {
316 empty_exponent_start,
317 trailing_junk_start,
318 } => {
319 if let Some(exponent_start) = empty_exponent_start {
320 err = Some("Missing digits after the exponent symbol".into());
321 err_range = Some(exponent_start..exponent_start + 1);
322 } else if (trailing_junk_start as usize) < token_text.len() {
323 err = Some("trailing junk after numeric literal".into());
324 err_range = Some(trailing_junk_start..token_text.len() as u32);
325 }
326 SyntaxKind::NUMERIC_NUMBER
327 }
328 squawk_lexer::LiteralKind::Str { terminated } => {
329 if !terminated {
330 err =
331 Some("Missing trailing `'` symbol to terminate the string literal".into());
332 } else if let Some(kind) = continuation {
333 self.validate_prefixed_string_content(token_text, 1, kind);
334 self.prefixed_string_continuation = Some(kind);
335 }
336 SyntaxKind::STRING
337 }
338 squawk_lexer::LiteralKind::NationalStr { terminated } => {
339 if !terminated {
340 err = Some(
341 "Missing trailing `'` symbol to terminate the national character string literal"
342 .into(),
343 );
344 }
345 SyntaxKind::NATIONAL_STRING
346 }
347 squawk_lexer::LiteralKind::ByteStr { terminated } => {
348 if !terminated {
349 err = Some(
350 "Missing trailing `'` symbol to terminate the hex bit string literal"
351 .into(),
352 );
353 } else {
354 self.validate_prefixed_string_content(token_text, 2, PrefixedStringKind::Byte);
355 self.prefixed_string_continuation = Some(PrefixedStringKind::Byte);
356 }
357 SyntaxKind::BYTE_STRING
358 }
359 squawk_lexer::LiteralKind::BitStr { terminated } => {
360 if !terminated {
361 err = Some(
362 "Missing trailing `'` symbol to terminate the bit string literal".into(),
363 );
364 } else {
365 self.validate_prefixed_string_content(token_text, 2, PrefixedStringKind::Bit);
366 self.prefixed_string_continuation = Some(PrefixedStringKind::Bit);
367 }
368 SyntaxKind::BIT_STRING
369 }
370 squawk_lexer::LiteralKind::DollarQuotedString { terminated } => {
371 if !terminated {
372 err = Some("Unterminated dollar quoted string literal".into());
374 }
375 SyntaxKind::DOLLAR_QUOTED_STRING
376 }
377 squawk_lexer::LiteralKind::UnicodeEscStr { terminated } => {
378 if !terminated {
379 err = Some(
380 "Missing trailing `'` symbol to terminate the unicode escape string literal"
381 .into(),
382 );
383 }
384 SyntaxKind::UNICODE_ESC_STRING
386 }
387 squawk_lexer::LiteralKind::EscStr { terminated } => {
388 if !terminated {
389 err = Some(
390 "Missing trailing `'` symbol to terminate the escape string literal".into(),
391 );
392 } else {
393 self.validate_prefixed_string_content(
394 token_text,
395 2,
396 PrefixedStringKind::Escape,
397 );
398 self.prefixed_string_continuation = Some(PrefixedStringKind::Escape);
399 }
400 SyntaxKind::ESC_STRING
401 }
402 };
403
404 let err = err
405 .as_deref()
406 .map(|msg| (msg, err_range.unwrap_or(0..token_text.len() as u32)));
407 self.push(syntax_kind, token_text.len(), err);
408 }
409
410 fn validate_prefixed_string_content(
411 &mut self,
412 token_text: &str,
413 inner_start: usize,
414 kind: PrefixedStringKind,
415 ) {
416 let inner = &token_text[inner_start..token_text.len() - 1];
417 match kind {
418 PrefixedStringKind::Bit => {
419 for (i, c) in inner.char_indices() {
420 if !matches!(c, '0' | '1') {
421 self.push_content_error(
422 format!(r#""{c}" is not a valid binary digit"#),
423 inner_start + i,
424 c.len_utf8(),
425 );
426 }
427 }
428 }
429 PrefixedStringKind::Byte => {
430 for (i, c) in inner.char_indices() {
431 if !c.is_ascii_hexdigit() {
432 self.push_content_error(
433 format!(r#""{c}" is not a valid hexadecimal digit"#),
434 inner_start + i,
435 c.len_utf8(),
436 );
437 }
438 }
439 }
440 PrefixedStringKind::Escape => {
441 let mut chars = inner.char_indices().peekable();
442 while let Some((escape_start, c)) = chars.next() {
443 if c != '\\' {
444 continue;
445 }
446 let Some((next_pos, next_c)) = chars.next() else {
447 break;
448 };
449 let (required, example) = match next_c {
450 'u' => (4usize, r"\uXXXX"),
451 'U' => (8usize, r"\UXXXXXXXX"),
452 _ => continue,
453 };
454 let mut end = next_pos + next_c.len_utf8();
455 let mut got_all = true;
456 for _ in 0..required {
457 match chars.peek() {
458 Some(&(i, ch)) if ch.is_ascii_hexdigit() => {
459 end = i + ch.len_utf8();
460 chars.next();
461 }
462 _ => {
463 got_all = false;
464 break;
465 }
466 }
467 }
468 if !got_all {
469 self.push_content_error(
470 format!("Unicode escape requires {required} hex digits: {example}"),
471 inner_start + escape_start,
472 end - escape_start,
473 );
474 }
475 }
476 }
477 }
478 }
479
480 fn push_content_error(&mut self, msg: String, start: usize, len: usize) {
481 let token_start = self.offset as u32;
482 let start = token_start + start as u32;
483 self.res.error.push(LexError {
484 msg,
485 range: start..start + len as u32,
486 });
487 }
488}
489
490#[cfg(test)]
491mod tests {
492 use annotate_snippets::{AnnotationKind, Level, Renderer, Snippet, renderer::DecorStyle};
493 use insta::{assert_debug_snapshot, assert_snapshot};
494
495 use super::LexedStr;
496
497 fn lex(text: &str) -> String {
498 let lexed = LexedStr::new(text);
499 let renderer = Renderer::plain().decor_style(DecorStyle::Unicode);
500 let mut res = String::new();
501
502 for (range, msg) in lexed.errors() {
503 let span = range.start as usize..range.end as usize;
504 let group = Level::ERROR.primary_title(msg).element(
505 Snippet::source(text)
506 .fold(true)
507 .annotation(AnnotationKind::Primary.span(span)),
508 );
509 res.push_str(&renderer.render(&[group]).to_string());
510 res.push('\n');
511 }
512
513 res
514 }
515
516 fn lex_errors(text: &str) -> Vec<(std::ops::Range<u32>, String)> {
517 LexedStr::new(text)
518 .errors()
519 .map(|(range, msg)| (range.clone(), msg.to_owned()))
520 .collect()
521 }
522
523 #[test]
524 fn prefixed_string_content_errors() {
525 assert_debug_snapshot!(lex_errors("B'102' X'1G' E'\\u00'"), @r#"
526 [
527 (
528 4..5,
529 "\"2\" is not a valid binary digit",
530 ),
531 (
532 10..11,
533 "\"G\" is not a valid hexadecimal digit",
534 ),
535 (
536 15..19,
537 "Unicode escape requires 4 hex digits: \\uXXXX",
538 ),
539 ]
540 "#);
541 }
542
543 #[test]
544 fn prefixed_string_continuations_use_the_initial_string_kind() {
545 assert_debug_snapshot!(lex_errors("B'0'\n'2'"), @r#"
546 [
547 (
548 6..7,
549 "\"2\" is not a valid binary digit",
550 ),
551 ]
552 "#);
553 assert_debug_snapshot!(lex_errors("X'F'\n'G'"), @r#"
554 [
555 (
556 6..7,
557 "\"G\" is not a valid hexadecimal digit",
558 ),
559 ]
560 "#);
561 assert_debug_snapshot!(lex_errors("E'ok'\n'\\u0'"), @r#"
562 [
563 (
564 7..10,
565 "Unicode escape requires 4 hex digits: \\uXXXX",
566 ),
567 ]
568 "#);
569 }
570
571 #[test]
572 fn prefixed_string_continuation_state_resets() {
573 assert_debug_snapshot!(
574 lex_errors("B'01' || '2'; X'0F' N'x' 'G'; E'ok' $tag$x$tag$ '\\u0'"),
575 @"[]"
576 );
577 assert_debug_snapshot!(lex_errors("B'01' 2 '2'; X'0F' 1.5 'G'"), @"[]");
578 }
579
580 #[test]
581 fn empty_int_error() {
582 assert_snapshot!(lex("select 0x;"), @"
583 error: Missing digits after the integer base prefix
584 ╭▸
585 1 │ select 0x;
586 ╰╴ ━━
587 ");
588 }
589
590 #[test]
591 fn empty_int_with_trailing_ident_error() {
592 assert_snapshot!(lex("select 0xg;"), @"
593 error: trailing junk after numeric literal
594 ╭▸
595 1 │ select 0xg;
596 ╰╴ ━
597 ");
598 }
599
600 #[test]
601 fn invalid_octal_digits_error() {
602 assert_snapshot!(lex("select 0o999;"), @"
603 error: invalid digit for a base 8 literal
604 ╭▸
605 1 │ select 0o999;
606 ╰╴ ━
607 error: invalid digit for a base 8 literal
608 ╭▸
609 1 │ select 0o999;
610 ╰╴ ━
611 error: invalid digit for a base 8 literal
612 ╭▸
613 1 │ select 0o999;
614 ╰╴ ━
615 ");
616 }
617
618 #[test]
619 fn invalid_binary_digits_error() {
620 assert_snapshot!(lex("select 0b234;"), @"
621 error: invalid digit for a base 2 literal
622 ╭▸
623 1 │ select 0b234;
624 ╰╴ ━
625 error: invalid digit for a base 2 literal
626 ╭▸
627 1 │ select 0b234;
628 ╰╴ ━
629 error: invalid digit for a base 2 literal
630 ╭▸
631 1 │ select 0b234;
632 ╰╴ ━
633 ");
634 }
635
636 #[test]
637 fn invalid_octal_digits_after_valid_error() {
638 assert_snapshot!(lex("select 0o7889;"), @"
639 error: invalid digit for a base 8 literal
640 ╭▸
641 1 │ select 0o7889;
642 ╰╴ ━
643 error: invalid digit for a base 8 literal
644 ╭▸
645 1 │ select 0o7889;
646 ╰╴ ━
647 error: invalid digit for a base 8 literal
648 ╭▸
649 1 │ select 0o7889;
650 ╰╴ ━
651 ");
652 }
653
654 #[test]
655 fn empty_exponent_error() {
656 assert_snapshot!(lex("select 1e;"), @"
657 error: Missing digits after the exponent symbol
658 ╭▸
659 1 │ select 1e;
660 ╰╴ ━
661 ");
662 }
663
664 #[test]
665 fn unterminated_string_error() {
666 assert_snapshot!(lex("select 'hello;"), @"
667 error: Missing trailing `'` symbol to terminate the string literal
668 ╭▸
669 1 │ select 'hello;
670 ╰╴ ━━━━━━━
671 ");
672 }
673
674 #[test]
675 fn unterminated_hex_bit_string_error() {
676 assert_snapshot!(lex("select X'1F;"), @"
677 error: Missing trailing `'` symbol to terminate the hex bit string literal
678 ╭▸
679 1 │ select X'1F;
680 ╰╴ ━━━━━
681 ");
682 }
683
684 #[test]
685 fn unterminated_bit_string_error() {
686 assert_snapshot!(lex("select B'101;"), @"
687 error: Missing trailing `'` symbol to terminate the bit string literal
688 ╭▸
689 1 │ select B'101;
690 ╰╴ ━━━━━━
691 ");
692 }
693
694 #[test]
695 fn unterminated_dollar_quoted_string_error() {
696 assert_snapshot!(lex("select $tag$hello;"), @"
697 error: Unterminated dollar quoted string literal
698 ╭▸
699 1 │ select $tag$hello;
700 ╰╴ ━━━━━━━━━━━
701 ");
702 }
703
704 #[test]
705 fn unterminated_unicode_escape_string_error() {
706 assert_snapshot!(lex("select U&'hello;"), @"
707 error: Missing trailing `'` symbol to terminate the unicode escape string literal
708 ╭▸
709 1 │ select U&'hello;
710 ╰╴ ━━━━━━━━━
711 ");
712 }
713
714 #[test]
715 fn unterminated_escape_string_error() {
716 assert_snapshot!(lex("select E'hello;"), @"
717 error: Missing trailing `'` symbol to terminate the escape string literal
718 ╭▸
719 1 │ select E'hello;
720 ╰╴ ━━━━━━━━
721 ");
722 }
723}