1use crate::syntax::SyntaxKind;
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct LexError {
22 pub message: String,
24 pub offset: Option<usize>,
26}
27
28impl std::fmt::Display for LexError {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 match self.offset {
31 Some(o) => write!(f, "{} (at byte {o})", self.message),
32 None => f.write_str(&self.message),
33 }
34 }
35}
36
37impl std::error::Error for LexError {}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct Token<'a> {
45 pub kind: SyntaxKind,
47 pub text: &'a str,
49}
50
51const BOM_STR: &str = "\u{FEFF}";
53
54pub fn validate_utf8(bytes: &[u8]) -> Result<&str, LexError> {
64 match std::str::from_utf8(bytes) {
65 Ok(s) => Ok(s),
66 Err(e) => Err(LexError {
67 message: "input is not valid UTF-8".to_string(),
68 offset: Some(e.valid_up_to()),
69 }),
70 }
71}
72
73#[must_use]
78pub fn tokenize(src: &str) -> Vec<Token<'_>> {
79 let mut lexer = Lexer::new(src);
80 let mut tokens = Vec::new();
81 while let Some(tok) = lexer.next_token() {
82 tokens.push(tok);
83 }
84 debug_assert_eq!(
85 tokens.iter().map(|t| t.text.len()).sum::<usize>(),
86 src.len(),
87 "lexer must cover every source byte exactly once (INV-1)"
88 );
89 tokens
90}
91
92struct Lexer<'a> {
93 src: &'a str,
94 pos: usize,
96 at_start: bool,
98}
99
100impl<'a> Lexer<'a> {
101 fn new(src: &'a str) -> Self {
102 Self {
103 src,
104 pos: 0,
105 at_start: true,
106 }
107 }
108
109 #[inline]
111 fn rest(&self) -> &'a str {
112 &self.src[self.pos..]
113 }
114
115 #[inline]
117 fn peek(&self) -> Option<char> {
118 self.rest().chars().next()
119 }
120
121 #[inline]
123 fn peek_nth(&self, n: usize) -> Option<char> {
124 self.rest().chars().nth(n)
125 }
126
127 #[inline]
129 fn emit(&self, kind: SyntaxKind, start: usize) -> Token<'a> {
130 Token {
131 kind,
132 text: &self.src[start..self.pos],
133 }
134 }
135
136 fn next_token(&mut self) -> Option<Token<'a>> {
137 if self.pos >= self.src.len() {
138 return None;
139 }
140 let start = self.pos;
141
142 if self.at_start && self.rest().starts_with(BOM_STR) {
144 self.pos += BOM_STR.len();
145 self.at_start = false;
146 return Some(self.emit(SyntaxKind::Bom, start));
147 }
148 self.at_start = false;
149
150 let c = self.peek().expect("non-empty rest has a char");
151
152 let token = match c {
153 c if is_whitespace(c) => self.lex_whitespace(start),
154 '/' if self.peek_nth(1) == Some('/') => self.lex_line_comment(start),
155 '/' if self.peek_nth(1) == Some('*') => self.lex_block_comment(start),
156 '"' => self.lex_string(start),
157 'r' if matches!(self.peek_nth(1), Some('"') | Some('#')) => self.lex_raw_string(start),
158 '\'' => self.lex_char(start),
159 '0'..='9' => self.lex_number(start, false),
160 '+' | '-' if matches!(self.peek_nth(1), Some('0'..='9') | Some('.')) => {
161 self.lex_number(start, true)
162 }
163 '.' if matches!(self.peek_nth(1), Some('0'..='9')) => self.lex_number(start, false),
164 c if is_ident_start(c) => self.lex_ident_or_keyword(start),
165 '(' => self.bump_punct(SyntaxKind::LParen, start),
166 ')' => self.bump_punct(SyntaxKind::RParen, start),
167 '[' => self.bump_punct(SyntaxKind::LBracket, start),
168 ']' => self.bump_punct(SyntaxKind::RBracket, start),
169 '{' => self.bump_punct(SyntaxKind::LBrace, start),
170 '}' => self.bump_punct(SyntaxKind::RBrace, start),
171 ':' => self.bump_punct(SyntaxKind::Colon, start),
172 ',' => self.bump_punct(SyntaxKind::Comma, start),
173 '#' => self.bump_punct(SyntaxKind::Hash, start),
174 '!' => self.bump_punct(SyntaxKind::Bang, start),
175 _ => {
177 self.pos += c.len_utf8();
178 self.emit(SyntaxKind::LexError, start)
179 }
180 };
181 Some(token)
182 }
183
184 #[inline]
186 fn bump_punct(&mut self, kind: SyntaxKind, start: usize) -> Token<'a> {
187 self.pos += 1;
189 self.emit(kind, start)
190 }
191
192 fn lex_whitespace(&mut self, start: usize) -> Token<'a> {
193 while let Some(c) = self.peek() {
194 if is_whitespace(c) {
195 self.pos += c.len_utf8();
196 } else {
197 break;
198 }
199 }
200 self.emit(SyntaxKind::Whitespace, start)
201 }
202
203 fn lex_line_comment(&mut self, start: usize) -> Token<'a> {
204 self.pos += 2;
206 while let Some(c) = self.peek() {
207 if c == '\n' {
208 break;
209 }
210 self.pos += c.len_utf8();
211 }
212 self.emit(SyntaxKind::LineComment, start)
213 }
214
215 fn lex_block_comment(&mut self, start: usize) -> Token<'a> {
216 self.pos += 2;
219 let mut depth = 1usize;
220 while depth > 0 {
221 let Some(c) = self.peek() else { break };
222 if c == '/' && self.peek_nth(1) == Some('*') {
223 self.pos += 2;
224 depth += 1;
225 } else if c == '*' && self.peek_nth(1) == Some('/') {
226 self.pos += 2;
227 depth -= 1;
228 } else {
229 self.pos += c.len_utf8();
230 }
231 }
232 self.emit(SyntaxKind::BlockComment, start)
233 }
234
235 fn lex_string(&mut self, start: usize) -> Token<'a> {
236 self.pos += 1;
238 while let Some(c) = self.peek() {
239 match c {
240 '\\' => {
241 self.pos += 1;
245 if let Some(esc) = self.peek() {
246 self.pos += esc.len_utf8();
247 }
248 }
249 '"' => {
250 self.pos += 1;
251 break;
252 }
253 _ => self.pos += c.len_utf8(),
254 }
255 }
256 self.emit(SyntaxKind::String, start)
257 }
258
259 fn lex_raw_string(&mut self, start: usize) -> Token<'a> {
260 self.pos += 1;
263 let mut hashes = 0usize;
265 while self.peek() == Some('#') {
266 self.pos += 1;
267 hashes += 1;
268 }
269 if self.peek() != Some('"') {
272 return self.emit(SyntaxKind::RawString, start);
273 }
274 self.pos += 1; while let Some(c) = self.peek() {
278 if c == '"' {
279 let after_quote = self.pos + 1;
281 let mut matched = 0usize;
282 let mut probe = after_quote;
283 while matched < hashes && self.src[probe..].starts_with('#') {
284 probe += 1;
285 matched += 1;
286 }
287 if matched == hashes {
288 self.pos = probe;
289 break;
290 }
291 self.pos += 1;
293 } else {
294 self.pos += c.len_utf8();
295 }
296 }
297 self.emit(SyntaxKind::RawString, start)
298 }
299
300 fn lex_char(&mut self, start: usize) -> Token<'a> {
301 self.pos += 1;
303 while let Some(c) = self.peek() {
304 match c {
305 '\\' => {
306 self.pos += 1;
307 if let Some(esc) = self.peek() {
308 self.pos += esc.len_utf8();
309 }
310 }
311 '\'' => {
312 self.pos += 1;
313 break;
314 }
315 _ => self.pos += c.len_utf8(),
316 }
317 }
318 self.emit(SyntaxKind::Char, start)
319 }
320
321 fn lex_number(&mut self, start: usize, signed: bool) -> Token<'a> {
323 if signed {
324 self.pos += 1; }
326
327 if self.peek() == Some('0') {
329 if let Some(radix) = self.peek_nth(1) {
330 let base = match radix {
331 'x' | 'X' => Some(16u32),
332 'b' | 'B' => Some(2),
333 'o' | 'O' => Some(8),
334 _ => None,
335 };
336 if let Some(base) = base {
337 self.pos += 2; self.consume_digits(base);
339 self.consume_type_suffix();
340 return self.emit(SyntaxKind::Integer, start);
341 }
342 }
343 }
344
345 self.consume_digits(10);
347
348 let mut is_float = false;
349
350 if self.peek() == Some('.') && self.peek_nth(1) != Some('.') {
355 is_float = true;
356 self.pos += 1;
357 self.consume_digits(10);
358 }
359
360 if matches!(self.peek(), Some('e') | Some('E')) {
362 let next = self.peek_nth(1);
364 let exp = matches!(next, Some('0'..='9'))
365 || (matches!(next, Some('+') | Some('-'))
366 && matches!(self.peek_nth(2), Some('0'..='9')));
367 if exp {
368 is_float = true;
369 self.pos += 1; if matches!(self.peek(), Some('+') | Some('-')) {
371 self.pos += 1;
372 }
373 self.consume_digits(10);
374 }
375 }
376
377 self.consume_type_suffix();
378
379 self.emit(
380 if is_float {
381 SyntaxKind::Float
382 } else {
383 SyntaxKind::Integer
384 },
385 start,
386 )
387 }
388
389 fn consume_digits(&mut self, base: u32) {
391 while let Some(c) = self.peek() {
392 if c == '_' || c.is_digit(base) {
393 self.pos += 1; } else {
395 break;
396 }
397 }
398 }
399
400 fn consume_type_suffix(&mut self) {
407 if let Some(c) = self.peek() {
409 if c.is_ascii_alphabetic() {
410 while let Some(c) = self.peek() {
411 if is_ident_continue(c) {
412 self.pos += c.len_utf8();
413 } else {
414 break;
415 }
416 }
417 }
418 }
419 }
420
421 fn lex_ident_or_keyword(&mut self, start: usize) -> Token<'a> {
422 while let Some(c) = self.peek() {
423 if is_ident_continue(c) {
424 self.pos += c.len_utf8();
425 } else {
426 break;
427 }
428 }
429 let text = &self.src[start..self.pos];
430 let kind = match text {
431 "true" => SyntaxKind::TrueKw,
432 "false" => SyntaxKind::FalseKw,
433 "enable" => SyntaxKind::EnableKw,
434 _ => SyntaxKind::Ident,
435 };
436 Token { kind, text }
437 }
438}
439
440#[inline]
442fn is_whitespace(c: char) -> bool {
443 c.is_whitespace()
446}
447
448#[inline]
450fn is_ident_start(c: char) -> bool {
451 c == '_' || c.is_alphabetic()
452}
453
454#[inline]
456fn is_ident_continue(c: char) -> bool {
457 c == '_' || c.is_alphanumeric()
458}
459
460#[cfg(test)]
461mod tests {
462 use super::*;
463
464 fn concat(tokens: &[Token<'_>]) -> String {
465 tokens.iter().map(|t| t.text).collect()
466 }
467
468 fn kinds(tokens: &[Token<'_>]) -> Vec<SyntaxKind> {
469 tokens.iter().map(|t| t.kind).collect()
470 }
471
472 #[test]
473 fn validate_utf8_accepts_valid() {
474 assert_eq!(validate_utf8(b"hello").unwrap(), "hello");
475 }
476
477 #[test]
478 fn validate_utf8_rejects_invalid_without_panic() {
479 let bad = [0xFF, 0xFE, 0x00];
480 let err = validate_utf8(&bad).unwrap_err();
481 assert_eq!(err.offset, Some(0));
482 }
483
484 #[test]
485 fn covers_every_byte() {
486 let inputs = [
487 "",
488 " ",
489 "// only a comment",
490 "/* nested /* block */ comment */",
491 "Foo(x: 1, y: 2.5)",
492 "[1, 2, 3,]",
493 "{ \"k\": 'c', 4: true }",
494 "r#\"raw \"quote\" string\"#",
495 "Some(())",
496 "#![enable(implicit_some)]\n42",
497 "0xFF_u8 0b1010 0o17 1_000.5e-3f64 -1 +2.0",
498 ];
499 for input in inputs {
500 let toks = tokenize(input);
501 assert_eq!(concat(&toks), input, "round-trip for {input:?}");
502 }
503 }
504
505 #[test]
506 fn leading_bom_is_trivia() {
507 let src = "\u{FEFF}1";
508 let toks = tokenize(src);
509 assert_eq!(toks[0].kind, SyntaxKind::Bom);
510 assert_eq!(toks[0].text, "\u{FEFF}");
511 assert_eq!(concat(&toks), src);
512 }
513
514 #[test]
515 fn raw_string_with_hashes() {
516 let src = "r##\"has \"# inside\"##";
517 let toks = tokenize(src);
518 assert_eq!(kinds(&toks), vec![SyntaxKind::RawString]);
519 assert_eq!(toks[0].text, src);
520 }
521
522 #[test]
523 fn numbers_classified() {
524 assert_eq!(tokenize("42")[0].kind, SyntaxKind::Integer);
525 assert_eq!(tokenize("0xFF")[0].kind, SyntaxKind::Integer);
526 assert_eq!(tokenize("3.14")[0].kind, SyntaxKind::Float);
527 assert_eq!(tokenize("1e10")[0].kind, SyntaxKind::Float);
528 assert_eq!(tokenize("1_000i64")[0].kind, SyntaxKind::Integer);
529 }
530
531 #[test]
532 fn keywords_and_idents() {
533 assert_eq!(tokenize("true")[0].kind, SyntaxKind::TrueKw);
534 assert_eq!(tokenize("false")[0].kind, SyntaxKind::FalseKw);
535 assert_eq!(tokenize("enable")[0].kind, SyntaxKind::EnableKw);
536 assert_eq!(tokenize("Foo")[0].kind, SyntaxKind::Ident);
537 }
538
539 #[test]
540 fn unterminated_string_runs_to_eof_without_panic() {
541 let src = "\"no end";
542 let toks = tokenize(src);
543 assert_eq!(toks[0].kind, SyntaxKind::String);
544 assert_eq!(concat(&toks), src);
545 }
546
547 #[test]
548 fn crlf_preserved() {
549 let src = "1\r\n2";
550 let toks = tokenize(src);
551 assert_eq!(concat(&toks), src);
552 }
553}