1use logos::Logos;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub struct Span {
10 pub start: usize,
11 pub end: usize,
12 pub line: u32,
13 pub column: u32,
14}
15
16impl Span {
17 #[must_use]
19 pub fn slice<'a>(&self, source: &'a str) -> &'a str {
20 &source[self.start..self.end]
21 }
22}
23
24#[derive(Debug, Clone, PartialEq)]
26pub struct Token {
27 pub kind: TokenKind,
28 pub span: Span,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
38pub enum LexErrorKind {
39 UnexpectedCharacter,
40 UnterminatedString,
41 InvalidNumber,
42 InvalidBinary,
43}
44
45impl LexErrorKind {
46 fn as_message(&self) -> &'static str {
47 match self {
48 Self::UnexpectedCharacter => "unexpected character",
49 Self::UnterminatedString => "unterminated string literal",
50 Self::InvalidNumber => "numeric literal out of range",
51 Self::InvalidBinary => "invalid binary literal",
52 }
53 }
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct LexError {
60 pub kind: LexErrorKind,
61 pub span: Span,
62 pub snippet: String,
63}
64
65impl std::fmt::Display for LexError {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 write!(
68 f,
69 "lex error at line {}, column {}: {} (snippet: {:?})",
70 self.span.line,
71 self.span.column,
72 self.kind.as_message(),
73 self.snippet,
74 )
75 }
76}
77
78impl std::error::Error for LexError {}
79
80pub fn tokenize(source: &str) -> Result<Vec<Token>, LexError> {
88 Lexer::new(source).collect()
89}
90
91pub struct Lexer<'src> {
100 source: &'src str,
101 inner: logos::Lexer<'src, TokenKind>,
102 prev_end: usize,
103 line: u32,
104 column: u32,
105 peeked: Option<Result<Token, LexError>>,
106}
107
108impl<'src> Lexer<'src> {
109 #[must_use]
110 pub fn new(source: &'src str) -> Self {
111 Self {
112 source,
113 inner: TokenKind::lexer(source),
114 prev_end: 0,
115 line: 1,
116 column: 1,
117 peeked: None,
118 }
119 }
120
121 pub fn peek(&mut self) -> Option<&Result<Token, LexError>> {
124 if self.peeked.is_none() {
125 self.peeked = self.next_from_inner();
126 }
127 self.peeked.as_ref()
128 }
129
130 fn advance_over(&mut self, start: usize, end: usize) {
132 if start >= end {
133 return;
134 }
135 for ch in self.source[start..end].chars() {
136 if ch == '\n' {
137 self.line += 1;
138 self.column = 1;
139 } else {
140 self.column += 1;
141 }
142 }
143 }
144
145 fn next_from_inner(&mut self) -> Option<Result<Token, LexError>> {
151 let kind_result = self.inner.next()?;
152 let range = self.inner.span();
153
154 self.advance_over(self.prev_end, range.start);
156
157 let token_line = self.line;
159 let token_column = self.column;
160
161 self.advance_over(range.start, range.end);
165 self.prev_end = range.end;
166
167 let span = Span {
168 start: range.start,
169 end: range.end,
170 line: token_line,
171 column: token_column,
172 };
173
174 let Ok(kind) = kind_result else {
175 let raw = span.slice(self.source);
176 let snippet = truncate_to_chars(raw, 40);
177 let mut cs = raw.chars();
181 let kind = match cs.next() {
182 Some(c) if c.is_ascii_digit() => LexErrorKind::InvalidNumber,
183 Some('#' | '+' | '-') if cs.next().is_some_and(|c| c.is_ascii_digit()) => {
184 LexErrorKind::InvalidNumber
185 }
186 _ => LexErrorKind::UnexpectedCharacter,
187 };
188 return Some(Err(LexError {
189 kind,
190 span,
191 snippet,
192 }));
193 };
194 Some(Ok(Token { kind, span }))
195 }
196}
197
198impl Iterator for Lexer<'_> {
199 type Item = Result<Token, LexError>;
200
201 fn next(&mut self) -> Option<Self::Item> {
202 if let Some(buffered) = self.peeked.take() {
203 return Some(buffered);
204 }
205 self.next_from_inner()
206 }
207}
208
209fn truncate_to_chars(s: &str, max_chars: usize) -> String {
212 match s.char_indices().nth(max_chars) {
213 Some((byte_idx, _)) => s[..byte_idx].to_string(),
214 None => s.to_string(),
215 }
216}
217
218#[derive(Logos, Debug, Clone, PartialEq)]
222#[logos(skip r"[ \t\r\n]+")]
223#[logos(skip r"/\*([^*]|\*+[^*/])*\*+/")]
224pub enum TokenKind {
225 #[regex(r"(?i)ISO-10303-21", priority = 10)]
232 IsoStart,
233
234 #[regex(r"(?i)END-ISO-10303-21", priority = 10)]
235 IsoEnd,
236
237 #[regex(r"(?i)HEADER", priority = 10)]
238 Header,
239
240 #[regex(r"(?i)DATA", priority = 10)]
241 Data,
242
243 #[regex(r"(?i)ENDSEC", priority = 10)]
244 EndSec,
245
246 #[token("(")]
247 LParen,
248
249 #[token(")")]
250 RParen,
251
252 #[token(",")]
253 Comma,
254
255 #[token(";")]
256 Semicolon,
257
258 #[token("=")]
259 Equals,
260
261 #[token("*")]
263 Asterisk,
264
265 #[token("$")]
267 Dollar,
268
269 #[regex(
271 r"[+-]?[0-9]+\.[0-9]*([Ee][+-]?[0-9]+)?",
272 |lex| lex.slice().parse::<f64>().ok().filter(|f| f.is_finite())
276 )]
277 Real(f64),
278
279 #[regex(
281 r"[+-]?[0-9]+",
282 |lex| lex.slice().parse::<i64>().ok()
283 )]
284 Integer(i64),
285
286 #[regex(
291 r"'([^']|'')*'",
292 |lex| {
293 let s = lex.slice();
294 s[1..s.len() - 1].replace("''", "'")
295 }
296 )]
297 String(String),
298
299 #[regex(
301 r"#[0-9]+",
302 |lex| lex.slice()[1..].parse::<u64>().ok()
303 )]
304 EntityRef(u64),
305
306 #[regex(
309 r"\.[A-Za-z_][A-Za-z0-9_]*\.",
310 |lex| {
311 let s = lex.slice();
312 s[1..s.len() - 1].to_string()
313 }
314 )]
315 Enum(String),
316
317 #[regex(
320 r#""[0-3][0-9A-Fa-f]*""#,
321 |lex| {
322 let s = lex.slice();
323 s[1..s.len() - 1].to_string()
324 }
325 )]
326 Binary(String),
327
328 #[regex(
331 r"[A-Za-z_][A-Za-z0-9_]*",
332 |lex| lex.slice().to_string()
333 )]
334 Keyword(String),
335
336 #[regex(r"<[^>]+>", |lex| lex.slice().to_string())]
341 AnchorRef(String),
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347
348 fn first_token(src: &str) -> TokenKind {
349 TokenKind::lexer(src)
350 .next()
351 .expect("expected at least one token")
352 .expect("expected Ok token")
353 }
354
355 #[test]
356 fn lex_punctuation() {
357 let mut lex = TokenKind::lexer("(),;=*$");
358 assert_eq!(lex.next(), Some(Ok(TokenKind::LParen)));
359 assert_eq!(lex.next(), Some(Ok(TokenKind::RParen)));
360 assert_eq!(lex.next(), Some(Ok(TokenKind::Comma)));
361 assert_eq!(lex.next(), Some(Ok(TokenKind::Semicolon)));
362 assert_eq!(lex.next(), Some(Ok(TokenKind::Equals)));
363 assert_eq!(lex.next(), Some(Ok(TokenKind::Asterisk)));
364 assert_eq!(lex.next(), Some(Ok(TokenKind::Dollar)));
365 assert_eq!(lex.next(), None);
366 }
367
368 #[test]
369 fn lex_integer_basic() {
370 assert_eq!(first_token("42"), TokenKind::Integer(42));
371 }
372
373 #[test]
374 fn lex_integer_zero() {
375 assert_eq!(first_token("0"), TokenKind::Integer(0));
376 }
377
378 #[test]
379 fn lex_integer_signed() {
380 assert_eq!(first_token("+7"), TokenKind::Integer(7));
381 assert_eq!(first_token("-13"), TokenKind::Integer(-13));
382 }
383
384 #[test]
385 fn lex_real_basic() {
386 assert_eq!(first_token("1.23"), TokenKind::Real(1.23));
387 }
388
389 #[test]
390 fn lex_real_trailing_dot() {
391 assert_eq!(first_token("0."), TokenKind::Real(0.0));
393 assert_eq!(first_token("100."), TokenKind::Real(100.0));
394 }
395
396 #[test]
397 fn lex_real_exponent() {
398 assert_eq!(first_token("1.E-07"), TokenKind::Real(1e-7));
399 assert_eq!(first_token("1.23e5"), TokenKind::Real(1.23e5));
400 }
401
402 #[test]
403 fn lex_real_signed_exponent() {
404 assert_eq!(first_token("-9.80E+02"), TokenKind::Real(-9.80e2));
405 }
406
407 #[test]
408 fn lex_real_wins_over_integer() {
409 let mut lex = TokenKind::lexer("1.23");
411 assert_eq!(lex.next(), Some(Ok(TokenKind::Real(1.23))));
412 assert_eq!(lex.next(), None);
413 }
414
415 #[test]
416 fn lex_integer_when_no_dot() {
417 let mut lex = TokenKind::lexer("1");
419 assert_eq!(lex.next(), Some(Ok(TokenKind::Integer(1))));
420 assert_eq!(lex.next(), None);
421 }
422
423 #[test]
424 fn lex_string_empty() {
425 assert_eq!(first_token("''"), TokenKind::String(String::new()));
426 }
427
428 #[test]
429 fn lex_string_simple() {
430 assert_eq!(first_token("'abc'"), TokenKind::String("abc".into()));
431 }
432
433 #[test]
434 fn lex_string_escaped_quote() {
435 assert_eq!(first_token("'a''b'"), TokenKind::String("a'b".into()));
437 }
438
439 #[test]
440 fn lex_string_only_escaped_quote() {
441 assert_eq!(first_token("''''"), TokenKind::String("'".into()));
443 }
444
445 #[test]
446 fn lex_string_with_newline() {
447 assert_eq!(
449 first_token("'line1\nline2'"),
450 TokenKind::String("line1\nline2".into())
451 );
452 }
453
454 #[test]
455 fn lex_string_multibyte_korean() {
456 assert_eq!(first_token("'한글'"), TokenKind::String("한글".into()));
459 }
460
461 #[test]
462 fn lex_string_multibyte_japanese() {
463 assert_eq!(first_token("'日本語'"), TokenKind::String("日本語".into()));
464 }
465
466 #[test]
467 fn lex_anchor_ref_simple() {
468 assert_eq!(
469 first_token("<TestAnchor>"),
470 TokenKind::AnchorRef("<TestAnchor>".into())
471 );
472 }
473
474 #[test]
475 fn lex_anchor_ref_with_url() {
476 assert_eq!(
477 first_token("<testAnchorAndData.stp#TestAnchor>"),
478 TokenKind::AnchorRef("<testAnchorAndData.stp#TestAnchor>".into())
479 );
480 }
481
482 #[test]
483 fn lex_entity_ref_small() {
484 assert_eq!(first_token("#1"), TokenKind::EntityRef(1));
485 }
486
487 #[test]
488 fn lex_entity_ref_large() {
489 assert_eq!(first_token("#1234567"), TokenKind::EntityRef(1_234_567));
490 }
491
492 #[test]
493 fn lex_enum_bool_true() {
494 assert_eq!(first_token(".T."), TokenKind::Enum("T".into()));
495 }
496
497 #[test]
498 fn lex_enum_unit() {
499 assert_eq!(first_token(".MILLI."), TokenKind::Enum("MILLI".into()));
500 }
501
502 #[test]
503 fn lex_binary_zero() {
504 assert_eq!(first_token("\"0\""), TokenKind::Binary("0".into()));
505 }
506
507 #[test]
508 fn lex_binary_hex() {
509 assert_eq!(first_token("\"3FFA\""), TokenKind::Binary("3FFA".into()));
510 }
511
512 #[test]
513 fn lex_keyword_simple() {
514 assert_eq!(
515 first_token("CARTESIAN_POINT"),
516 TokenKind::Keyword("CARTESIAN_POINT".into())
517 );
518 }
519
520 #[test]
521 fn lex_keyword_leading_underscore() {
522 assert_eq!(first_token("_x1"), TokenKind::Keyword("_x1".into()));
523 }
524
525 #[test]
526 fn lex_keyword_mixed_case() {
527 assert_eq!(first_token("PlAnE"), TokenKind::Keyword("PlAnE".into()));
529 }
530
531 #[test]
532 fn lex_section_iso_start_upper() {
533 assert_eq!(first_token("ISO-10303-21"), TokenKind::IsoStart);
534 }
535
536 #[test]
537 fn lex_section_iso_start_lower() {
538 assert_eq!(first_token("iso-10303-21"), TokenKind::IsoStart);
540 }
541
542 #[test]
543 fn lex_section_iso_end() {
544 assert_eq!(first_token("END-ISO-10303-21"), TokenKind::IsoEnd);
545 }
546
547 #[test]
548 fn lex_section_header_data_endsec() {
549 assert_eq!(first_token("HEADER"), TokenKind::Header);
550 assert_eq!(first_token("DATA"), TokenKind::Data);
551 assert_eq!(first_token("ENDSEC"), TokenKind::EndSec);
552 }
553
554 #[test]
555 fn lex_section_case_insensitive() {
556 assert_eq!(first_token("header"), TokenKind::Header);
557 assert_eq!(first_token("Data"), TokenKind::Data);
558 assert_eq!(first_token("EndSec"), TokenKind::EndSec);
559 }
560
561 #[test]
562 fn section_keyword_priority_wins_over_keyword() {
563 assert_ne!(first_token("HEADER"), TokenKind::Keyword("HEADER".into()));
566 }
567
568 #[test]
569 fn lex_whitespace_skipped_between_tokens() {
570 let mut lex = TokenKind::lexer("HEADER ;\n\tDATA");
571 assert_eq!(lex.next(), Some(Ok(TokenKind::Header)));
572 assert_eq!(lex.next(), Some(Ok(TokenKind::Semicolon)));
573 assert_eq!(lex.next(), Some(Ok(TokenKind::Data)));
574 assert_eq!(lex.next(), None);
575 }
576
577 #[test]
578 fn lex_comment_skipped_between_tokens() {
579 let mut lex = TokenKind::lexer("1 /* ignored */ 2");
580 assert_eq!(lex.next(), Some(Ok(TokenKind::Integer(1))));
581 assert_eq!(lex.next(), Some(Ok(TokenKind::Integer(2))));
582 assert_eq!(lex.next(), None);
583 }
584
585 #[test]
586 fn lex_multiline_comment_skipped() {
587 let mut lex = TokenKind::lexer("1/* line1\nline2 */2");
588 assert_eq!(lex.next(), Some(Ok(TokenKind::Integer(1))));
589 assert_eq!(lex.next(), Some(Ok(TokenKind::Integer(2))));
590 assert_eq!(lex.next(), None);
591 }
592
593 #[test]
594 fn lex_minimal_entity_line() {
595 let mut lex = TokenKind::lexer("#1=LINE('',#2,#3);");
597 assert_eq!(lex.next(), Some(Ok(TokenKind::EntityRef(1))));
598 assert_eq!(lex.next(), Some(Ok(TokenKind::Equals)));
599 assert_eq!(lex.next(), Some(Ok(TokenKind::Keyword("LINE".into()))));
600 assert_eq!(lex.next(), Some(Ok(TokenKind::LParen)));
601 assert_eq!(lex.next(), Some(Ok(TokenKind::String(String::new()))));
602 assert_eq!(lex.next(), Some(Ok(TokenKind::Comma)));
603 assert_eq!(lex.next(), Some(Ok(TokenKind::EntityRef(2))));
604 assert_eq!(lex.next(), Some(Ok(TokenKind::Comma)));
605 assert_eq!(lex.next(), Some(Ok(TokenKind::EntityRef(3))));
606 assert_eq!(lex.next(), Some(Ok(TokenKind::RParen)));
607 assert_eq!(lex.next(), Some(Ok(TokenKind::Semicolon)));
608 assert_eq!(lex.next(), None);
609 }
610
611 fn collect(src: &str) -> Vec<Token> {
614 Lexer::new(src)
615 .collect::<Result<Vec<_>, _>>()
616 .expect("expected all tokens to lex successfully")
617 }
618
619 #[test]
620 fn span_tracks_single_token() {
621 let tokens = collect("HEADER");
622 assert_eq!(tokens.len(), 1);
623 assert_eq!(tokens[0].kind, TokenKind::Header);
624 assert_eq!(tokens[0].span.start, 0);
625 assert_eq!(tokens[0].span.end, 6);
626 assert_eq!(tokens[0].span.line, 1);
627 assert_eq!(tokens[0].span.column, 1);
628 }
629
630 #[test]
631 fn span_tracks_columns_on_same_line() {
632 let tokens = collect("a b");
634 assert_eq!(tokens[0].span.column, 1);
635 assert_eq!(tokens[1].span.column, 3);
636 assert_eq!(tokens[0].span.line, 1);
637 assert_eq!(tokens[1].span.line, 1);
638 }
639
640 #[test]
641 fn span_tracks_line_after_newline() {
642 let tokens = collect("a\nb");
644 assert_eq!(tokens[0].span.line, 1);
645 assert_eq!(tokens[0].span.column, 1);
646 assert_eq!(tokens[1].span.line, 2);
647 assert_eq!(tokens[1].span.column, 1);
648 }
649
650 #[test]
651 fn span_tracks_multiple_newlines() {
652 let tokens = collect("a\n\n\nb");
653 assert_eq!(tokens[1].span.line, 4);
654 assert_eq!(tokens[1].span.column, 1);
655 }
656
657 #[test]
658 fn span_advances_past_crlf() {
659 let tokens = collect("a\r\nb");
661 assert_eq!(tokens[1].span.line, 2);
662 assert_eq!(tokens[1].span.column, 1);
663 }
664
665 #[test]
666 fn span_tracks_column_after_multibyte_char() {
667 let tokens = collect("'한글' a");
670 assert_eq!(tokens.len(), 2);
671 assert!(matches!(tokens[0].kind, TokenKind::String(_)));
672 assert_eq!(tokens[1].span.column, 6); assert_eq!(tokens[1].span.line, 1);
674 }
675
676 #[test]
677 fn span_advances_through_multiline_string() {
678 let tokens = collect("'line1\nline2' x");
681 assert!(matches!(tokens[0].kind, TokenKind::String(_)));
682 assert_eq!(tokens[1].span.line, 2);
685 assert_eq!(tokens[1].span.column, 8);
686 }
687
688 #[test]
689 fn span_slice_roundtrip_matches_source() {
690 let source = "#1=LINE('',#2,#3);";
691 let tokens = collect(source);
692 for tok in &tokens {
693 let slice = tok.span.slice(source);
694 assert_eq!(&source[tok.span.start..tok.span.end], slice);
696 }
697 }
698
699 #[test]
702 fn peek_returns_same_token_twice() {
703 let mut lex = Lexer::new("HEADER ; DATA");
704 let first = lex.peek().cloned();
705 let second = lex.peek().cloned();
706 assert_eq!(first, second);
707 assert!(matches!(
708 first,
709 Some(Ok(Token {
710 kind: TokenKind::Header,
711 ..
712 }))
713 ));
714 }
715
716 #[test]
717 fn peek_then_next_returns_buffered_token() {
718 let mut lex = Lexer::new("HEADER ; DATA");
719 let peeked = lex.peek().cloned();
720 let next = lex.next();
721 assert_eq!(peeked, next);
722 }
723
724 #[test]
725 fn peek_does_not_consume_token() {
726 let mut lex = Lexer::new("HEADER ; DATA");
727 let _ = lex.peek();
728 assert!(matches!(
731 lex.next(),
732 Some(Ok(Token {
733 kind: TokenKind::Header,
734 ..
735 }))
736 ));
737 assert!(matches!(
738 lex.next(),
739 Some(Ok(Token {
740 kind: TokenKind::Semicolon,
741 ..
742 }))
743 ));
744 assert!(matches!(
745 lex.next(),
746 Some(Ok(Token {
747 kind: TokenKind::Data,
748 ..
749 }))
750 ));
751 assert_eq!(lex.next(), None);
752 }
753
754 #[test]
755 fn peek_at_end_returns_none() {
756 let mut lex = Lexer::new("HEADER");
757 let _ = lex.next();
758 assert!(lex.peek().is_none());
759 assert!(lex.next().is_none());
760 }
761
762 #[test]
765 fn tokenize_returns_vec_of_tokens() {
766 let toks = tokenize("HEADER ; ENDSEC ;").expect("should lex cleanly");
767 let kinds: Vec<_> = toks.iter().map(|t| t.kind.clone()).collect();
768 assert_eq!(
769 kinds,
770 vec![
771 TokenKind::Header,
772 TokenKind::Semicolon,
773 TokenKind::EndSec,
774 TokenKind::Semicolon,
775 ]
776 );
777 }
778
779 #[test]
780 fn tokenize_reports_unexpected_character() {
781 let err = tokenize("#1 = @INVALID").expect_err("expected a lex error");
782 assert_eq!(err.kind, LexErrorKind::UnexpectedCharacter);
783 assert!(err.snippet.contains('@'));
784 assert_eq!(err.span.line, 1);
785 }
786
787 #[test]
788 fn tokenize_reports_unterminated_string_as_error() {
789 let err = tokenize("'abc").expect_err("unterminated string must error");
791 assert!(matches!(
794 err.kind,
795 LexErrorKind::UnexpectedCharacter | LexErrorKind::UnterminatedString
796 ));
797 }
798
799 #[test]
800 fn lex_error_display_has_line_column_and_snippet() {
801 let err = tokenize("\n @").expect_err("expected a lex error");
802 let msg = err.to_string();
803 assert!(msg.contains("line 2"));
804 assert!(msg.contains("column 3"));
805 assert!(msg.contains("unexpected character"));
806 assert!(msg.contains('@'));
807 }
808
809 #[test]
810 fn lex_error_implements_std_error() {
811 fn assert_error<E: std::error::Error>(_: &E) {}
812 let err = tokenize("@").expect_err("expected a lex error");
813 assert_error(&err);
814 }
815}