Coverage Report

Created: 2025-09-05 15:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/noah/src/ruchy/src/frontend/lexer.rs
Line
Count
Source
1
//! Lexical analysis and tokenization
2
3
use crate::frontend::ast::Span;
4
use logos::{Lexer, Logos};
5
6
/// Process escape sequences in a string literal
7
110
fn process_escapes(s: &str) -> String {
8
110
    let mut result = String::new();
9
110
    let mut chars = s.chars();
10
11
1.66k
    while let Some(
ch1.55k
) = chars.next() {
12
1.55k
        if ch == '\\' {
13
1
            match chars.next() {
14
1
                Some('n') => result.push('\n'),
15
0
                Some('t') => result.push('\t'),
16
0
                Some('r') => result.push('\r'),
17
0
                Some('\\') | None => result.push('\\'), // Backslash or end of string
18
0
                Some('"') => result.push('"'),
19
0
                Some('\'') => result.push('\''),
20
0
                Some('0') => result.push('\0'),
21
0
                Some('u') if chars.as_str().starts_with('{') => {
22
                    // Unicode escape: \u{XXXX}
23
0
                    chars.next(); // consume '{'
24
0
                    let mut hex = String::new();
25
0
                    for hex_char in chars.by_ref() {
26
0
                        if hex_char == '}' {
27
0
                            break;
28
0
                        }
29
0
                        hex.push(hex_char);
30
                    }
31
32
0
                    if let Ok(code_point) = u32::from_str_radix(&hex, 16) {
33
0
                        if let Some(unicode_char) = char::from_u32(code_point) {
34
0
                            result.push(unicode_char);
35
0
                        } else {
36
0
                            // Invalid Unicode code point, keep as literal
37
0
                            result.push_str("\\u{");
38
0
                            result.push_str(&hex);
39
0
                            result.push('}');
40
0
                        }
41
0
                    } else {
42
0
                        // Invalid hex, keep as literal
43
0
                        result.push_str("\\u{");
44
0
                        result.push_str(&hex);
45
0
                        result.push('}');
46
0
                    }
47
                }
48
0
                Some(other) => {
49
0
                    // Unknown escape sequence, keep as literal
50
0
                    result.push('\\');
51
0
                    result.push(other);
52
0
                }
53
            }
54
1.55k
        } else {
55
1.55k
            result.push(ch);
56
1.55k
        }
57
    }
58
59
110
    result
60
110
}
61
62
1.73k
#[derive(Logos, Debug, PartialEq, Clone)]
63
#[logos(skip r"[ \t\n\f]+")]
64
#[logos(skip r"//[^\n]*")]
65
#[logos(skip r"/\*([^*]|\*[^/])*\*/")]
66
pub enum Token {
67
    // Literals
68
    #[regex(r"[0-9]+(?:i8|i16|i32|i64|i128|isize|u8|u16|u32|u64|u128|usize)?", |lex| {
69
        let slice = lex.slice();
70
        // Strip type suffix for parsing the numeric value
71
1.18k
        let (num_part, _type_suffix) = if let Some(pos) = slice.find(|c: char| c.is_alphabetic()) {
72
            (&slice[..pos], Some(&slice[pos..]))
73
        } else {
74
            (slice, None)
75
        };
76
        num_part.parse::<i64>().ok()
77
    })]
78
    Integer(i64),
79
80
    #[regex(r"[0-9]+\.[0-9]+([eE][+-]?[0-9]+)?", |lex| lex.slice().parse::<f64>().ok())]
81
    Float(f64),
82
83
    #[regex(r#""([^"\\]|\\.)*""#, |lex| {
84
        let s = lex.slice();
85
        let inner = &s[1..s.len()-1];
86
        Some(process_escapes(inner))
87
    })]
88
    String(String),
89
90
    #[regex(r#"f"([^"\\]|\\.)*""#, |lex| {
91
        let s = lex.slice();
92
        // Remove f" prefix and " suffix
93
        let inner = &s[2..s.len()-1];
94
        Some(process_escapes(inner))
95
    })]
96
    FString(String),
97
98
    #[regex(r#"r"([^"])*""#, |lex| {
99
        let s = lex.slice();
100
        // Remove r" prefix and " suffix - no escape processing for raw strings
101
        Some(s[2..s.len()-1].to_string())
102
    })]
103
    RawString(String),
104
105
    #[regex(r"'([^'\\]|\\.)'", |lex| {
106
        let s = lex.slice();
107
        let inner = &s[1..s.len()-1];
108
        if inner.len() == 1 {
109
            inner.chars().next()
110
        } else if inner.starts_with('\\') && inner.len() == 2 {
111
            match inner.chars().nth(1) {
112
                Some('n') => Some('\n'),
113
                Some('t') => Some('\t'),
114
                Some('r') => Some('\r'),
115
                Some('\\') => Some('\\'),
116
                Some('\'') => Some('\''),
117
                Some('0') => Some('\0'),
118
                _ => None,
119
            }
120
        } else {
121
            None
122
        }
123
    })]
124
    Char(char),
125
126
    #[token("true", |_| true)]
127
    #[token("false", |_| false)]
128
    Bool(bool),
129
130
    // Keywords
131
    #[token("fun")]
132
    Fun,
133
    #[token("fn")]
134
    Fn,
135
    #[token("let")]
136
    Let,
137
    #[token("var")]
138
    Var,
139
    #[token("mod")]
140
    Mod,
141
    #[token("if")]
142
    If,
143
    #[token("else")]
144
    Else,
145
    #[token("match")]
146
    Match,
147
    #[token("for")]
148
    For,
149
    #[token("in")]
150
    In,
151
    #[token("while")]
152
    While,
153
    #[token("loop")]
154
    Loop,
155
    #[token("async")]
156
    Async,
157
    #[token("await")]
158
    Await,
159
    #[token("throw")]
160
    Throw,
161
    #[token("try")]
162
    Try,
163
    #[token("catch")]
164
    Catch,
165
    #[token("return")]
166
    Return,
167
    #[token("command")]
168
    Command,
169
    #[token("Ok")]
170
    Ok,
171
    #[token("Err")]
172
    Err,
173
    #[token("Some")]
174
    Some,
175
    #[token("None")]
176
    None,
177
    #[token("null")]
178
    Null,
179
    #[token("Result")]
180
    Result,
181
    #[token("Option")]
182
    Option,
183
    #[token("break")]
184
    Break,
185
    #[token("continue")]
186
    Continue,
187
    #[token("struct")]
188
    Struct,
189
    #[token("enum")]
190
    Enum,
191
    #[token("impl")]
192
    Impl,
193
    #[token("trait")]
194
    Trait,
195
    #[token("extend")]
196
    Extend,
197
    #[token("actor")]
198
    Actor,
199
    #[token("state")]
200
    State,
201
    #[token("receive")]
202
    Receive,
203
    #[token("send")]
204
    Send,
205
    #[token("ask")]
206
    Ask,
207
    #[token("type")]
208
    Type,
209
    #[token("where")]
210
    Where,
211
    #[token("const")]
212
    Const,
213
    #[token("static")]
214
    Static,
215
    #[token("mut")]
216
    Mut,
217
    #[token("pub")]
218
    Pub,
219
    #[token("import")]
220
    Import,
221
    #[token("use")]
222
    Use,
223
    #[token("as")]
224
    As,
225
    #[token("module")]
226
    Module,
227
    #[token("export")]
228
    Export,
229
    #[token("df", priority = 2)]
230
    DataFrame,
231
232
    // Identifiers (lower priority than keywords)
233
    #[regex(r"[a-zA-Z_][a-zA-Z0-9_]*", |lex| lex.slice().to_string(), priority = 1)]
234
    Identifier(String),
235
236
    // Operators
237
    #[token("+")]
238
    Plus,
239
    #[token("-")]
240
    Minus,
241
    #[token("*")]
242
    Star,
243
    #[token("/")]
244
    Slash,
245
    #[token("%")]
246
    Percent,
247
    #[token("**")]
248
    Power,
249
250
    #[token("==")]
251
    EqualEqual,
252
    #[token("!=")]
253
    NotEqual,
254
    #[token("<?")]
255
    ActorQuery,
256
    #[token("<-")]
257
    LeftArrow,
258
    #[token("<")]
259
    Less,
260
    #[token("<=")]
261
    LessEqual,
262
    #[token(">")]
263
    Greater,
264
    #[token(">=")]
265
    GreaterEqual,
266
267
    #[token("&&")]
268
    AndAnd,
269
    #[token("||")]
270
    OrOr,
271
    #[token("!")]
272
    Bang,
273
274
    #[token("&")]
275
    Ampersand,
276
    #[token("|")]
277
    Pipe,
278
    #[token("^")]
279
    Caret,
280
    #[token("~")]
281
    Tilde,
282
    #[token("\\")]
283
    Backslash,
284
    #[token("<<")]
285
    LeftShift,
286
287
    #[token("=")]
288
    Equal,
289
    #[token("+=")]
290
    PlusEqual,
291
    #[token("-=")]
292
    MinusEqual,
293
    #[token("*=")]
294
    StarEqual,
295
    #[token("/=")]
296
    SlashEqual,
297
    #[token("%=")]
298
    PercentEqual,
299
    #[token("**=")]
300
    PowerEqual,
301
    #[token("&=")]
302
    AmpersandEqual,
303
    #[token("|=")]
304
    PipeEqual,
305
    #[token("^=")]
306
    CaretEqual,
307
    #[token("<<=")]
308
    LeftShiftEqual,
309
310
    #[token("++")]
311
    Increment,
312
    #[token("--")]
313
    Decrement,
314
315
    #[token("|>")]
316
    Pipeline,
317
    #[token("->")]
318
    Arrow,
319
    #[token("=>")]
320
    FatArrow,
321
    #[token("..")]
322
    DotDot,
323
    #[token("..=")]
324
    DotDotEqual,
325
    #[token("...")]
326
    DotDotDot,
327
    #[token("??")]
328
    NullCoalesce,
329
    #[token("?")]
330
    Question,
331
    #[token("?.")]
332
    SafeNav,
333
334
    // Delimiters
335
    #[token("(")]
336
    LeftParen,
337
    #[token(")")]
338
    RightParen,
339
    #[token("[")]
340
    LeftBracket,
341
    #[token("]")]
342
    RightBracket,
343
    #[token("{")]
344
    LeftBrace,
345
    #[token("}")]
346
    RightBrace,
347
348
    // Punctuation
349
    #[token(",")]
350
    Comma,
351
    #[token(".")]
352
    Dot,
353
    #[token(":")]
354
    Colon,
355
    #[token("::")]
356
    ColonColon,
357
    #[token(";")]
358
    Semicolon,
359
    #[token("_", priority = 2)]
360
    Underscore,
361
362
    // Attribute support
363
    #[token("#")]
364
    Hash,
365
}
366
367
impl Token {
368
    #[must_use]
369
1.14k
    pub fn is_binary_op(&self) -> bool {
370
566
        matches!(
371
1.14k
            self,
372
            Token::Plus
373
                | Token::Minus
374
                | Token::Star
375
                | Token::Slash
376
                | Token::Percent
377
                | Token::Power
378
                | Token::EqualEqual
379
                | Token::NotEqual
380
                | Token::Less
381
                | Token::LessEqual
382
                | Token::Greater
383
                | Token::GreaterEqual
384
                | Token::AndAnd
385
                | Token::OrOr
386
                | Token::Ampersand
387
                | Token::Pipe
388
                | Token::Caret
389
                | Token::LeftShift
390
        )
391
1.14k
    }
392
393
    #[must_use]
394
0
    pub fn is_unary_op(&self) -> bool {
395
0
        matches!(
396
0
            self,
397
            Token::Bang | Token::Minus | Token::Tilde | Token::Ampersand
398
        )
399
0
    }
400
401
    #[must_use]
402
646
    pub fn is_assignment_op(&self) -> bool {
403
646
        matches!(
404
646
            self,
405
            Token::Equal
406
                | Token::PlusEqual
407
                | Token::MinusEqual
408
                | Token::StarEqual
409
                | Token::SlashEqual
410
                | Token::PercentEqual
411
                | Token::PowerEqual
412
                | Token::AmpersandEqual
413
                | Token::PipeEqual
414
                | Token::CaretEqual
415
                | Token::LeftShiftEqual
416
        )
417
646
    }
418
}
419
420
pub struct TokenStream<'a> {
421
    lexer: Lexer<'a, Token>,
422
    peeked: Option<(Token, Span)>,
423
}
424
425
/// Saved position in the token stream for backtracking
426
#[derive(Clone)]
427
pub struct TokenStreamPosition<'a> {
428
    lexer: Lexer<'a, Token>,
429
    peeked: Option<(Token, Span)>,
430
}
431
432
impl<'a> TokenStream<'a> {
433
    #[must_use]
434
1.09k
    pub fn new(input: &'a str) -> Self {
435
1.09k
        Self {
436
1.09k
            lexer: Token::lexer(input),
437
1.09k
            peeked: None,
438
1.09k
        }
439
1.09k
    }
440
441
    /// Save the current position for later restoration
442
    #[must_use]
443
54
    pub fn position(&self) -> TokenStreamPosition<'a> {
444
54
        TokenStreamPosition {
445
54
            lexer: self.lexer.clone(),
446
54
            peeked: self.peeked.clone(),
447
54
        }
448
54
    }
449
450
    /// Restore a previously saved position
451
49
    pub fn set_position(&mut self, pos: TokenStreamPosition<'a>) {
452
49
        self.lexer = pos.lexer;
453
49
        self.peeked = pos.peeked;
454
49
    }
455
456
    #[allow(clippy::should_implement_trait)]
457
12.4k
    pub fn next(&mut self) -> Option<(Token, Span)> {
458
12.4k
        if let Some(
peeked4.62k
) = self.peeked.take() {
459
4.62k
            return Some(peeked);
460
7.78k
        }
461
462
7.78k
        self.lexer.next().map(|result| 
{5.12k
463
5.12k
            let token = result.unwrap_or(Token::Bang); // Error recovery
464
5.12k
            let span = Span::new(self.lexer.span().start, self.lexer.span().end);
465
5.12k
            (token, span)
466
5.12k
        })
467
12.4k
    }
468
469
12.4k
    pub fn peek(&mut self) -> Option<&(Token, Span)> {
470
12.4k
        if self.peeked.is_none() {
471
7.66k
            self.peeked = self.next();
472
7.66k
        
}4.83k
473
12.4k
        self.peeked.as_ref()
474
12.4k
    }
475
476
0
    pub fn peek_nth(&mut self, n: usize) -> Option<(Token, Span)> {
477
        // For simplicity, we'll only support peek_nth(1) by cloning the lexer
478
0
        if n == 1 {
479
0
            let saved_peeked = self.peeked.clone();
480
0
            let saved_lexer = self.lexer.clone();
481
482
            // Get first token
483
0
            let _first = self.peek();
484
0
            self.advance();
485
486
            // Get second token
487
0
            let result = self.peek().cloned();
488
489
            // Restore state
490
0
            self.lexer = saved_lexer;
491
0
            self.peeked = saved_peeked;
492
493
0
            result
494
        } else {
495
0
            None // Not supported for n > 1
496
        }
497
0
    }
498
499
0
    pub fn peek_nth_is_colon(&mut self, n: usize) -> bool {
500
0
        if n == 0 {
501
0
            self.peek().is_some_and(|(t, _)| matches!(t, Token::Colon))
502
        } else {
503
0
            self.peek_nth(n)
504
0
                .is_some_and(|(t, _)| matches!(t, Token::Colon))
505
        }
506
0
    }
507
508
    /// Expect a specific token and return its span
509
    ///
510
    /// # Errors
511
    ///
512
    /// Returns an error if the next token doesn't match the expected token or if we reached EOF
513
804
    pub fn expect(&mut self, expected: &Token) -> anyhow::Result<Span> {
514
804
        match self.next() {
515
804
            Some((token, span)) if token == *expected => Ok(span),
516
0
            Some((token, _)) => anyhow::bail!("Expected {:?}, found {:?}", expected, token),
517
0
            None => anyhow::bail!("Expected {:?}, found EOF", expected),
518
        }
519
804
    }
520
521
    // Alias for next() to avoid clippy warning about Iterator trait
522
3.93k
    pub fn advance(&mut self) -> Option<(Token, Span)> {
523
3.93k
        self.next()
524
3.93k
    }
525
}
526
527
#[cfg(test)]
528
#[allow(clippy::unwrap_used)]
529
#[allow(clippy::panic)]
530
mod tests {
531
    use super::*;
532
    use proptest::prelude::*;
533
534
    #[test]
535
    #[allow(clippy::approx_constant)] // Intentional literal for test
536
1
    fn test_tokenize_basic() {
537
1
        let mut stream = TokenStream::new("let x = 42 + 3.14");
538
539
1
        assert_eq!(stream.next().map(|(t, _)| t), Some(Token::Let));
540
1
        assert_eq!(
541
1
            stream.next().map(|(t, _)| t),
542
1
            Some(Token::Identifier("x".to_string()))
543
        );
544
1
        assert_eq!(stream.next().map(|(t, _)| t), Some(Token::Equal));
545
1
        assert_eq!(stream.next().map(|(t, _)| t), Some(Token::Integer(42)));
546
1
        assert_eq!(stream.next().map(|(t, _)| t), Some(Token::Plus));
547
1
        assert_eq!(stream.next().map(|(t, _)| t), Some(Token::Float(3.14))); // Intentional literal for test
548
1
        assert_eq!(stream.next().map(|(t, _)| t), None);
549
1
    }
550
551
    #[test]
552
1
    fn test_tokenize_pipeline() {
553
1
        let mut stream = TokenStream::new("[1, 2, 3] >> map(|x| x * 2)");
554
555
1
        assert_eq!(stream.next().map(|(t, _)| t), Some(Token::LeftBracket));
556
1
        assert_eq!(stream.next().map(|(t, _)| t), Some(Token::Integer(1)));
557
        // ... rest of tokens
558
1
    }
559
560
    #[test]
561
1
    fn test_tokenize_comments() {
562
1
        let mut stream = TokenStream::new("x // comment\n+ /* block */ y");
563
564
1
        assert_eq!(
565
1
            stream.next().map(|(t, _)| t),
566
1
            Some(Token::Identifier("x".to_string()))
567
        );
568
1
        assert_eq!(stream.next().map(|(t, _)| t), Some(Token::Plus));
569
1
        assert_eq!(
570
1
            stream.next().map(|(t, _)| t),
571
1
            Some(Token::Identifier("y".to_string()))
572
        );
573
1
    }
574
575
    proptest! {
576
        #[test]
577
        fn test_tokenize_identifiers(s in "[a-zA-Z_][a-zA-Z0-9_]{0,100}") {
578
            // Skip reserved keywords that should tokenize as their respective tokens
579
            let reserved_keywords = [
580
                "true", "false", "fun", "fn", "let", "var", "mod", "if", "else", "match",
581
                "for", "in", "while", "loop", "async", "await", "throw", "try", "catch",
582
                "return", "command", "Ok", "Err", "Some", "None", "null", "Result", "Option",
583
                "break", "continue", "struct", "enum", "impl", "trait", "extend", "actor",
584
                "state", "receive", "send", "ask", "type", "where", "const", "static",
585
                "mut", "pub", "import", "use", "as", "module", "export", "df"
586
            ];
587
            
588
            if reserved_keywords.contains(&s.as_str()) {
589
                return Ok(()); // Skip test for reserved keywords
590
            }
591
            
592
            let mut stream = TokenStream::new(&s);
593
            match stream.advance() {
594
                Some((Token::Identifier(id), _)) => prop_assert_eq!(id, s),
595
                Some((Token::Underscore, _)) if s == "_" => {}, // Special case for underscore
596
                _ => panic!("Failed to tokenize identifier: {s}"),
597
            }
598
        }
599
600
        #[test]
601
        fn test_tokenize_integers(n in 0i64..1_000_000) {
602
            let s = n.to_string();
603
            let mut stream = TokenStream::new(&s);
604
            match stream.advance() {
605
                Some((Token::Integer(i), _)) => prop_assert_eq!(i, n),
606
                _ => panic!("Failed to tokenize integer"),
607
            }
608
        }
609
    }
610
}