Skip to main content

aver/
lexer.rs

1use std::fmt;
2use thiserror::Error;
3
4#[derive(Debug, Clone, PartialEq)]
5pub enum TokenKind {
6    // Literals
7    Int(i64),
8    /// A decimal integer literal whose magnitude overflows `i64`. The digits are
9    /// validated at lex time; the parser turns this into `Literal::BigInt`, which
10    /// every backend lowers through its arbitrary-precision `Int` construction.
11    BigInt(String),
12    Float(f64),
13    Str(String),
14    InterpStr(Vec<(bool, String)>), // (is_expr, text)
15    Bool(bool),
16    // Identifiers
17    Ident(String),
18    // Keywords
19    Module,
20    Depends,
21    Exposes,
22    Intent,
23    Type,
24    Record,
25    Fn,
26    Effects,
27    Decision,
28    Verify,
29    Match,
30    // Operators
31    Arrow,    // ->
32    FatArrow, // =>
33    Eq,       // ==
34    Neq,      // !=
35    Lte,      // <=
36    Gte,      // >=
37    Assign,   // =
38    Bang,     // !
39    Question, // ?
40    Lt,       // <
41    Gt,       // >
42    Plus,     // +
43    Minus,    // -
44    Star,     // *
45    Slash,    // /
46    Dot,      // .
47    Colon,    // :
48    Comma,    // ,
49    LParen,   // (
50    RParen,   // )
51    LBracket, // [
52    RBracket, // ]
53    LBrace,   // {
54    RBrace,   // }
55    // Structure
56    Indent,
57    Dedent,
58    Newline,
59    Eof,
60}
61
62impl fmt::Display for TokenKind {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        match self {
65            TokenKind::Int(n) => write!(f, "integer '{}'", n),
66            TokenKind::BigInt(s) => write!(f, "integer '{}'", s),
67            TokenKind::Float(n) => write!(f, "float '{}'", n),
68            TokenKind::Str(s) => write!(f, "string \"{}\"", s),
69            TokenKind::InterpStr(_) => write!(f, "interpolated string"),
70            TokenKind::Bool(b) => write!(f, "'{}'", b),
71            TokenKind::Ident(s) => write!(f, "'{}'", s),
72            TokenKind::Module => write!(f, "'module'"),
73            TokenKind::Depends => write!(f, "'depends'"),
74            TokenKind::Exposes => write!(f, "'exposes'"),
75            TokenKind::Intent => write!(f, "'intent'"),
76            TokenKind::Type => write!(f, "'type'"),
77            TokenKind::Record => write!(f, "'record'"),
78            TokenKind::Fn => write!(f, "'fn'"),
79            TokenKind::Effects => write!(f, "'effects'"),
80            TokenKind::Decision => write!(f, "'decision'"),
81            TokenKind::Verify => write!(f, "'verify'"),
82            TokenKind::Match => write!(f, "'match'"),
83            TokenKind::Arrow => write!(f, "'->'"),
84            TokenKind::FatArrow => write!(f, "'=>'"),
85            TokenKind::Eq => write!(f, "'=='"),
86            TokenKind::Neq => write!(f, "'!='"),
87            TokenKind::Lte => write!(f, "'<='"),
88            TokenKind::Gte => write!(f, "'>='"),
89            TokenKind::Assign => write!(f, "'='"),
90            TokenKind::Bang => write!(f, "'!'"),
91            TokenKind::Question => write!(f, "'?'"),
92            TokenKind::Lt => write!(f, "'<'"),
93            TokenKind::Gt => write!(f, "'>'"),
94            TokenKind::Plus => write!(f, "'+'"),
95            TokenKind::Minus => write!(f, "'-'"),
96            TokenKind::Star => write!(f, "'*'"),
97            TokenKind::Slash => write!(f, "'/'"),
98            TokenKind::Dot => write!(f, "'.'"),
99            TokenKind::Colon => write!(f, "':'"),
100            TokenKind::Comma => write!(f, "','"),
101            TokenKind::LParen => write!(f, "'('"),
102            TokenKind::RParen => write!(f, "')'"),
103            TokenKind::LBracket => write!(f, "'['"),
104            TokenKind::RBracket => write!(f, "']'"),
105            TokenKind::LBrace => write!(f, "'{{'"),
106            TokenKind::RBrace => write!(f, "'}}'"),
107            TokenKind::Indent => write!(f, "indentation"),
108            TokenKind::Dedent => write!(f, "end of block"),
109            TokenKind::Newline => write!(f, "end of line"),
110            TokenKind::Eof => write!(f, "end of file"),
111        }
112    }
113}
114
115#[derive(Debug, Clone)]
116pub struct Token {
117    pub kind: TokenKind,
118    pub line: usize,
119    pub col: usize,
120}
121
122#[derive(Debug, Error)]
123pub enum LexerError {
124    #[error("error[{line}:{col}]: {msg}")]
125    Error {
126        msg: String,
127        line: usize,
128        col: usize,
129    },
130}
131
132fn keyword(s: &str) -> Option<TokenKind> {
133    match s {
134        "module" => Some(TokenKind::Module),
135        "depends" => Some(TokenKind::Depends),
136        "exposes" => Some(TokenKind::Exposes),
137        "intent" => Some(TokenKind::Intent),
138        "type" => Some(TokenKind::Type),
139        "record" => Some(TokenKind::Record),
140        "fn" => Some(TokenKind::Fn),
141        "effects" => Some(TokenKind::Effects),
142        "decision" => Some(TokenKind::Decision),
143        "verify" => Some(TokenKind::Verify),
144        "match" => Some(TokenKind::Match),
145        "true" => Some(TokenKind::Bool(true)),
146        "false" => Some(TokenKind::Bool(false)),
147        _ => None,
148    }
149}
150
151pub struct Lexer {
152    chars: Vec<char>,
153    pos: usize,
154    line: usize,
155    col: usize,
156    indent_stack: Vec<usize>,
157    at_line_start: bool,
158}
159
160impl Lexer {
161    pub fn new(source: &str) -> Self {
162        Lexer {
163            chars: source.chars().collect(),
164            pos: 0,
165            line: 1,
166            col: 1,
167            indent_stack: vec![0],
168            at_line_start: true,
169        }
170    }
171
172    fn error(&self, msg: impl Into<String>) -> LexerError {
173        LexerError::Error {
174            msg: msg.into(),
175            line: self.line,
176            col: self.col,
177        }
178    }
179
180    fn peek(&self, offset: usize) -> Option<char> {
181        self.chars.get(self.pos + offset).copied()
182    }
183
184    fn current(&self) -> Option<char> {
185        self.chars.get(self.pos).copied()
186    }
187
188    fn advance(&mut self) -> Option<char> {
189        let ch = self.chars.get(self.pos).copied()?;
190        self.pos += 1;
191        if ch == '\n' {
192            self.line += 1;
193            self.col = 1;
194        } else {
195            self.col += 1;
196        }
197        Some(ch)
198    }
199
200    pub fn tokenize(&mut self) -> Result<Vec<Token>, LexerError> {
201        let mut tokens = Vec::new();
202
203        while self.pos < self.chars.len() {
204            self.scan_token(&mut tokens)?;
205        }
206
207        // Emit remaining DEDENTs
208        while self.indent_stack.len() > 1 {
209            self.indent_stack.pop();
210            tokens.push(Token {
211                kind: TokenKind::Dedent,
212                line: self.line,
213                col: self.col,
214            });
215        }
216
217        tokens.push(Token {
218            kind: TokenKind::Eof,
219            line: self.line,
220            col: self.col,
221        });
222
223        Ok(tokens)
224    }
225
226    fn scan_token(&mut self, tokens: &mut Vec<Token>) -> Result<(), LexerError> {
227        if self.at_line_start {
228            self.handle_indentation(tokens)?;
229            if self.pos >= self.chars.len() {
230                return Ok(());
231            }
232        }
233
234        let ch = match self.current() {
235            Some(c) => c,
236            None => return Ok(()),
237        };
238
239        // Skip spaces (not at line start)
240        if ch == ' ' {
241            self.advance();
242            return Ok(());
243        }
244
245        // Newline
246        if ch == '\n' {
247            let line = self.line;
248            let col = self.col;
249            self.advance();
250
251            let last_is_structural = tokens
252                .last()
253                .map(|t| {
254                    matches!(
255                        t.kind,
256                        TokenKind::Newline | TokenKind::Indent | TokenKind::Dedent
257                    )
258                })
259                .unwrap_or(true);
260
261            if !tokens.is_empty() && !last_is_structural {
262                tokens.push(Token {
263                    kind: TokenKind::Newline,
264                    line,
265                    col,
266                });
267            }
268            self.at_line_start = true;
269            return Ok(());
270        }
271
272        // Carriage return
273        if ch == '\r' {
274            self.advance();
275            return Ok(());
276        }
277
278        // Comments
279        if ch == '/' && self.peek(1) == Some('/') {
280            self.skip_comment();
281            return Ok(());
282        }
283
284        // Strings
285        if ch == '"' {
286            let tok = self.scan_string()?;
287            tokens.push(tok);
288            return Ok(());
289        }
290
291        // Numbers
292        if ch.is_ascii_digit() {
293            let tok = self.scan_number()?;
294            tokens.push(tok);
295            return Ok(());
296        }
297
298        // Identifiers / keywords
299        if ch.is_alphabetic() || ch == '_' {
300            let tok = self.scan_identifier();
301            tokens.push(tok);
302            return Ok(());
303        }
304
305        // Operators
306        let tok = self.scan_operator()?;
307        tokens.push(tok);
308        Ok(())
309    }
310
311    fn handle_indentation(&mut self, tokens: &mut Vec<Token>) -> Result<(), LexerError> {
312        self.at_line_start = false;
313        let mut indent = 0;
314
315        while self.pos < self.chars.len() && self.chars[self.pos] == ' ' {
316            indent += 1;
317            self.pos += 1;
318            self.col += 1;
319        }
320
321        // Empty line or comment-only line — don't emit indent/dedent
322        if self.pos < self.chars.len() {
323            let ch = self.chars[self.pos];
324            if ch == '\n' || ch == '\r' {
325                return Ok(());
326            }
327            if ch == '/' && self.pos + 1 < self.chars.len() && self.chars[self.pos + 1] == '/' {
328                return Ok(());
329            }
330        } else {
331            return Ok(());
332        }
333
334        let current = *self.indent_stack.last().unwrap();
335        let line = self.line;
336
337        if indent > current {
338            self.indent_stack.push(indent);
339            tokens.push(Token {
340                kind: TokenKind::Indent,
341                line,
342                col: 1,
343            });
344        } else if indent < current {
345            while self.indent_stack.len() > 1 && *self.indent_stack.last().unwrap() > indent {
346                self.indent_stack.pop();
347                tokens.push(Token {
348                    kind: TokenKind::Dedent,
349                    line,
350                    col: 1,
351                });
352            }
353            if *self.indent_stack.last().unwrap() != indent {
354                return Err(self.error(format!(
355                    "Invalid indentation level: {indent}. Aver uses \
356                     significant indentation with one consistent step per \
357                     block — every line in the same block dedents back to \
358                     a previously-opened indent level. Common cause: a \
359                     wrapped `fn` signature or a multi-line argument list \
360                     (Aver doesn't support either — keep each declaration \
361                     on a single line, or split the body into a named \
362                     helper function)."
363                )));
364            }
365        }
366
367        Ok(())
368    }
369
370    fn skip_comment(&mut self) {
371        while self.pos < self.chars.len() && self.chars[self.pos] != '\n' {
372            self.advance();
373        }
374    }
375
376    fn scan_string(&mut self) -> Result<Token, LexerError> {
377        let line = self.line;
378        let col = self.col;
379        self.advance(); // consume opening "
380
381        let mut parts: Vec<(bool, String)> = Vec::new(); // (is_expr, text)
382        let mut current = String::new();
383        let mut has_interp = false;
384
385        loop {
386            match self.current() {
387                None => return Err(self.error("Unterminated string literal")),
388                Some('"') => {
389                    self.advance();
390                    break;
391                }
392                Some('{') => {
393                    // {{ → literal {, otherwise start interpolation
394                    if self.chars.get(self.pos + 1).copied() == Some('{') {
395                        current.push('{');
396                        self.advance(); // first {
397                        self.advance(); // second {
398                    } else {
399                        has_interp = true;
400                        if !current.is_empty() {
401                            parts.push((false, current.clone()));
402                            current.clear();
403                        }
404                        self.advance(); // consume {
405                        let mut expr_text = String::new();
406                        let mut depth = 1usize;
407                        while self.pos < self.chars.len() && depth > 0 {
408                            match self.chars[self.pos] {
409                                '{' => {
410                                    depth += 1;
411                                    expr_text.push('{');
412                                    self.advance();
413                                }
414                                '}' => {
415                                    depth -= 1;
416                                    if depth > 0 {
417                                        expr_text.push('}');
418                                    }
419                                    self.advance();
420                                }
421                                c => {
422                                    expr_text.push(c);
423                                    self.advance();
424                                }
425                            }
426                        }
427                        parts.push((true, expr_text));
428                    }
429                }
430                Some('}') => {
431                    // }} → literal }, single } is just a literal character
432                    if self.chars.get(self.pos + 1).copied() == Some('}') {
433                        current.push('}');
434                        self.advance(); // first }
435                        self.advance(); // second }
436                    } else {
437                        current.push('}');
438                        self.advance();
439                    }
440                }
441                Some('\\') => {
442                    self.advance();
443                    match self.advance() {
444                        Some('b') => current.push('\u{0008}'),
445                        Some('f') => current.push('\u{000C}'),
446                        Some('n') => current.push('\n'),
447                        Some('t') => current.push('\t'),
448                        Some('r') => current.push('\r'),
449                        Some('"') => current.push('"'),
450                        Some('\\') => current.push('\\'),
451                        Some(c) => current.push(c),
452                        None => return Err(self.error("Unterminated string literal")),
453                    }
454                }
455                Some('\n') => return Err(self.error("Unterminated string literal")),
456                Some(c) => {
457                    current.push(c);
458                    self.advance();
459                }
460            }
461        }
462
463        if !current.is_empty() {
464            parts.push((false, current));
465        }
466
467        if has_interp {
468            Ok(Token {
469                kind: TokenKind::InterpStr(parts),
470                line,
471                col,
472            })
473        } else {
474            let plain = parts.into_iter().map(|(_, s)| s).collect::<String>();
475            Ok(Token {
476                kind: TokenKind::Str(plain),
477                line,
478                col,
479            })
480        }
481    }
482
483    fn scan_number(&mut self) -> Result<Token, LexerError> {
484        let line = self.line;
485        let col = self.col;
486        let mut num_str = String::new();
487        let mut is_float = false;
488
489        while let Some(c) = self.current() {
490            if c.is_ascii_digit() {
491                num_str.push(c);
492                self.advance();
493            } else {
494                break;
495            }
496        }
497
498        if self.current() == Some('.') && self.peek(1).map(|c| c.is_ascii_digit()).unwrap_or(false)
499        {
500            is_float = true;
501            num_str.push('.');
502            self.advance(); // consume '.'
503            while let Some(c) = self.current() {
504                if c.is_ascii_digit() {
505                    num_str.push(c);
506                    self.advance();
507                } else {
508                    break;
509                }
510            }
511        }
512
513        if is_float {
514            let f: f64 = num_str
515                .parse()
516                .map_err(|_| self.error("Invalid floating-point number"))?;
517            Ok(Token {
518                kind: TokenKind::Float(f),
519                line,
520                col,
521            })
522        } else {
523            // Aver's `Int` is arbitrary-precision (ℤ) at runtime AND in source: a
524            // literal that fits `i64` becomes `TokenKind::Int` (the byte-identical
525            // common path), while one whose magnitude overflows 64 bits becomes
526            // `TokenKind::BigInt` carrying the validated decimal digits. The parser
527            // lowers that to `Literal::BigInt`, which every backend constructs via
528            // the same arbitrary-precision path `Int.n("…")` uses. `num_str` here is
529            // pure ASCII digits (no sign, no separators), so the overflow case is a
530            // valid bignum magnitude — only a genuinely malformed literal errors.
531            match num_str.parse::<i64>() {
532                Ok(i) => Ok(Token {
533                    kind: TokenKind::Int(i),
534                    line,
535                    col,
536                }),
537                Err(e)
538                    if matches!(
539                        e.kind(),
540                        std::num::IntErrorKind::PosOverflow | std::num::IntErrorKind::NegOverflow
541                    ) =>
542                {
543                    Ok(Token {
544                        kind: TokenKind::BigInt(num_str),
545                        line,
546                        col,
547                    })
548                }
549                Err(_) => Err(self.error("Invalid integer literal")),
550            }
551        }
552    }
553
554    fn scan_identifier(&mut self) -> Token {
555        let line = self.line;
556        let col = self.col;
557        let mut ident = String::new();
558
559        while let Some(c) = self.current() {
560            if c.is_alphanumeric() || c == '_' {
561                ident.push(c);
562                self.advance();
563            } else {
564                break;
565            }
566        }
567
568        let kind = keyword(&ident).unwrap_or(TokenKind::Ident(ident));
569        Token { kind, line, col }
570    }
571
572    fn scan_operator(&mut self) -> Result<Token, LexerError> {
573        let line = self.line;
574        let col = self.col;
575        let ch = self.advance().unwrap();
576
577        let kind = match ch {
578            '-' if self.current() == Some('>') => {
579                self.advance();
580                TokenKind::Arrow
581            }
582            '=' if self.current() == Some('>') => {
583                self.advance();
584                TokenKind::FatArrow
585            }
586            '=' if self.current() == Some('=') => {
587                self.advance();
588                TokenKind::Eq
589            }
590            '!' if self.current() == Some('=') => {
591                self.advance();
592                TokenKind::Neq
593            }
594            '<' if self.current() == Some('=') => {
595                self.advance();
596                TokenKind::Lte
597            }
598            '>' if self.current() == Some('=') => {
599                self.advance();
600                TokenKind::Gte
601            }
602            '=' => TokenKind::Assign,
603            '<' => TokenKind::Lt,
604            '>' => TokenKind::Gt,
605            '+' => TokenKind::Plus,
606            '-' => TokenKind::Minus,
607            '*' => TokenKind::Star,
608            '/' => TokenKind::Slash,
609            '!' => TokenKind::Bang,
610            '?' => TokenKind::Question,
611            '.' => TokenKind::Dot,
612            ':' => TokenKind::Colon,
613            ',' => TokenKind::Comma,
614            '(' => TokenKind::LParen,
615            ')' => TokenKind::RParen,
616            '[' => TokenKind::LBracket,
617            ']' => TokenKind::RBracket,
618            '{' => TokenKind::LBrace,
619            '}' => TokenKind::RBrace,
620            other => return Err(self.error(format!("Unknown character: {:?}", other))),
621        };
622
623        Ok(Token { kind, line, col })
624    }
625}