Skip to main content

panproto_expr_parser/
lexer.rs

1//! Lexer producing a sequence of spanned tokens from source text.
2//!
3//! Uses logos for fast regex-based tokenization, then applies a layout
4//! insertion pass to convert indentation into explicit `Indent`/`Dedent`/
5//! `Newline` tokens (the GHC approach).
6
7use logos::Logos;
8
9use crate::token::{Span, Spanned, Token};
10
11/// Tokenize source text into a sequence of spanned tokens.
12///
13/// This performs two passes:
14/// 1. Raw tokenization via logos (skips whitespace within lines).
15/// 2. Layout insertion (converts indentation to virtual tokens).
16///
17/// # Errors
18///
19/// Returns an error if the input contains an unrecognized token.
20pub fn tokenize(input: &str) -> Result<Vec<Spanned>, LexError> {
21    let raw = raw_tokenize(input)?;
22    Ok(insert_layout(input, &raw))
23}
24
25/// A lexer error with source location.
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct LexError {
28    /// Byte offset of the unrecognized token.
29    pub offset: usize,
30    /// The problematic character(s).
31    pub text: String,
32}
33
34impl std::fmt::Display for LexError {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        write!(
37            f,
38            "unrecognized token at byte {}: {:?}",
39            self.offset, self.text
40        )
41    }
42}
43
44impl std::error::Error for LexError {}
45
46/// Raw tokenization via logos (no layout insertion).
47fn raw_tokenize(input: &str) -> Result<Vec<Spanned>, LexError> {
48    let mut tokens = Vec::new();
49    let mut lexer = Token::lexer(input);
50
51    while let Some(result) = lexer.next() {
52        let span = lexer.span();
53        if let Ok(token) = result {
54            tokens.push(Spanned {
55                token,
56                span: Span {
57                    start: span.start,
58                    end: span.end,
59                },
60            });
61        } else {
62            // Check if this is a newline (which logos skips).
63            let slice = &input[span.clone()];
64            if slice.contains('\n') || slice.contains('\r') {
65                // Newlines are handled by the layout pass, not as tokens.
66                continue;
67            }
68            return Err(LexError {
69                offset: span.start,
70                text: slice.to_string(),
71            });
72        }
73    }
74
75    tokens.push(Spanned {
76        token: Token::Eof,
77        span: Span {
78            start: input.len(),
79            end: input.len(),
80        },
81    });
82
83    Ok(tokens)
84}
85
86/// Layout insertion pass (GHC-style).
87///
88/// Scans the raw token stream and the original source text. When a layout
89/// keyword (`let`, `where`, `do`, `of`) is followed by a newline and
90/// increased indentation, inserts `Indent`. When indentation decreases,
91/// inserts `Dedent`. At the same indentation, inserts `Newline` to
92/// separate declarations.
93///
94/// If the layout keyword is followed by `{`, layout is suppressed
95/// (explicit delimiters).
96fn insert_layout(input: &str, raw: &[Spanned]) -> Vec<Spanned> {
97    if raw.is_empty() {
98        return vec![];
99    }
100
101    let mut result = Vec::with_capacity(raw.len());
102    let mut indent_stack: Vec<usize> = vec![0]; // column stack
103    let mut cursor = Cursor::new();
104    let mut prev_line = 0;
105    let mut prev_end = 0;
106
107    for spanned in raw {
108        cursor.advance_to(input, spanned.span.start);
109        let cur_line = cursor.line;
110        let cur_col = cursor.column();
111
112        // If we moved to a new line, check indentation.
113        if cur_line > prev_line {
114            let current_indent = *indent_stack.last().unwrap_or(&0);
115
116            match cur_col.cmp(&current_indent) {
117                std::cmp::Ordering::Greater => {
118                    // Check if previous token was a layout keyword.
119                    let prev_is_layout = result.last().is_some_and(|s: &Spanned| {
120                        matches!(s.token, Token::Let | Token::Where | Token::Do | Token::Of)
121                    });
122                    if prev_is_layout {
123                        indent_stack.push(cur_col);
124                        result.push(Spanned {
125                            token: Token::Indent,
126                            span: Span {
127                                start: spanned.span.start,
128                                end: spanned.span.start,
129                            },
130                        });
131                    }
132                }
133                std::cmp::Ordering::Less => {
134                    // Dedent: pop indent stack until we match or go below.
135                    while indent_stack.len() > 1 && *indent_stack.last().unwrap_or(&0) > cur_col {
136                        indent_stack.pop();
137                        result.push(Spanned {
138                            token: Token::Dedent,
139                            span: Span {
140                                start: spanned.span.start,
141                                end: spanned.span.start,
142                            },
143                        });
144                    }
145                }
146                std::cmp::Ordering::Equal => {
147                    // Same indentation: insert Newline separator.
148                    // Only if we're inside a layout block (indent_stack.len() > 1).
149                    if indent_stack.len() > 1 {
150                        result.push(Spanned {
151                            token: Token::Newline,
152                            span: Span {
153                                start: spanned.span.start,
154                                end: spanned.span.start,
155                            },
156                        });
157                    }
158                }
159            }
160        }
161
162        result.push(spanned.clone());
163        prev_line = cur_line;
164        prev_end = spanned.span.end;
165    }
166
167    // Close any remaining open layout blocks.
168    while indent_stack.len() > 1 {
169        indent_stack.pop();
170        result.push(Spanned {
171            token: Token::Dedent,
172            span: Span {
173                start: prev_end,
174                end: prev_end,
175            },
176        });
177    }
178
179    result
180}
181
182/// A line-and-column position walked forward through the source.
183///
184/// The layout pass reads every token's position in ascending offset order, so
185/// the cursor advances over each byte of the input exactly once across the
186/// whole pass. Recomputing a position from the start of the input instead
187/// would make the pass quadratic in the source's length.
188struct Cursor {
189    /// Byte offset the cursor has advanced to.
190    offset: usize,
191    /// 0-indexed line number at `offset`.
192    line: usize,
193    /// Byte offset of the start of the line holding `offset`.
194    line_start: usize,
195}
196
197impl Cursor {
198    const fn new() -> Self {
199        Self {
200            offset: 0,
201            line: 0,
202            line_start: 0,
203        }
204    }
205
206    /// Advance to `target`, counting the lines crossed on the way.
207    ///
208    /// `target` is never behind the cursor: the token stream is in ascending
209    /// offset order.
210    fn advance_to(&mut self, input: &str, target: usize) {
211        let target = target.max(self.offset).min(input.len());
212        for (index, byte) in input.as_bytes()[self.offset..target].iter().enumerate() {
213            if *byte == b'\n' {
214                self.line += 1;
215                self.line_start = self.offset + index + 1;
216            }
217        }
218        self.offset = target;
219    }
220
221    /// The 0-indexed column: bytes from the start of the current line.
222    const fn column(&self) -> usize {
223        self.offset - self.line_start
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    #[test]
232    fn simple_expression() {
233        let tokens = tokenize("1 + 2").unwrap_or_default();
234        assert_eq!(tokens[0].token, Token::Int(1));
235        assert_eq!(tokens[1].token, Token::Plus);
236        assert_eq!(tokens[2].token, Token::Int(2));
237        assert_eq!(tokens[3].token, Token::Eof);
238    }
239
240    #[test]
241    fn keywords_recognized() {
242        let tokens = tokenize("let x = 1 in x").unwrap_or_default();
243        assert_eq!(tokens[0].token, Token::Let);
244        assert_eq!(tokens[1].token, Token::Ident("x".into()));
245        assert_eq!(tokens[2].token, Token::Eq);
246        assert_eq!(tokens[3].token, Token::Int(1));
247        assert_eq!(tokens[4].token, Token::In);
248    }
249
250    #[test]
251    fn string_literal() {
252        let tokens = tokenize(r#""hello world""#).unwrap_or_default();
253        assert_eq!(tokens[0].token, Token::Str("hello world".into()));
254    }
255
256    #[test]
257    fn operators() {
258        let tokens = tokenize("a -> b && c || d").unwrap_or_default();
259        assert_eq!(tokens[0].token, Token::Ident("a".into()));
260        assert_eq!(tokens[1].token, Token::Arrow);
261        assert_eq!(tokens[2].token, Token::Ident("b".into()));
262        assert_eq!(tokens[3].token, Token::AndAnd);
263        assert_eq!(tokens[5].token, Token::OrOr);
264    }
265
266    #[test]
267    fn layout_let_block() {
268        let input = "let\n  x = 1\n  y = 2\nin x";
269        let tokens = tokenize(input).unwrap_or_default();
270        let kinds: Vec<&Token> = tokens.iter().map(|s| &s.token).collect();
271        // Should have: Let, Indent, Ident(x), Eq, Int(1), Newline,
272        //              Ident(y), Eq, Int(2), Dedent, In, Ident(x), Eof
273        assert!(kinds.contains(&&Token::Indent));
274        assert!(kinds.contains(&&Token::Newline));
275        assert!(kinds.contains(&&Token::Dedent));
276    }
277
278    #[test]
279    fn comprehension_tokens() {
280        let tokens = tokenize("[ a | a <- xs, a > 0 ]").unwrap_or_default();
281        assert_eq!(tokens[0].token, Token::LBracket);
282        assert_eq!(tokens[1].token, Token::Ident("a".into()));
283        assert_eq!(tokens[2].token, Token::Pipe);
284        assert_eq!(tokens[3].token, Token::Ident("a".into()));
285        assert_eq!(tokens[4].token, Token::LeftArrow);
286    }
287
288    #[test]
289    fn comment_skipped() {
290        let tokens = tokenize("x -- this is a comment\ny").unwrap_or_default();
291        let idents: Vec<&str> = tokens
292            .iter()
293            .filter_map(|s| {
294                if let Token::Ident(ref name) = s.token {
295                    Some(name.as_str())
296                } else {
297                    None
298                }
299            })
300            .collect();
301        assert_eq!(idents, vec!["x", "y"]);
302    }
303
304    #[test]
305    fn float_literal() {
306        let tokens = tokenize("3.125").unwrap_or_default();
307        assert!(matches!(tokens[0].token, Token::Float(f) if (f - 3.125).abs() < f64::EPSILON));
308    }
309
310    #[test]
311    fn hex_literal() {
312        let tokens = tokenize("0xFF").unwrap_or_default();
313        assert_eq!(tokens[0].token, Token::Int(255));
314    }
315
316    #[test]
317    fn upper_ident() {
318        let tokens = tokenize("True Nothing MyType").unwrap_or_default();
319        assert_eq!(tokens[0].token, Token::True);
320        assert_eq!(tokens[1].token, Token::Nothing);
321        assert_eq!(tokens[2].token, Token::UpperIdent("MyType".into()));
322    }
323
324    #[test]
325    fn lambda_tokens() {
326        let tokens = tokenize("\\x -> x + 1").unwrap_or_default();
327        assert_eq!(tokens[0].token, Token::Backslash);
328        assert_eq!(tokens[1].token, Token::Ident("x".into()));
329        assert_eq!(tokens[2].token, Token::Arrow);
330    }
331
332    #[test]
333    fn edge_traversal() {
334        let tokens = tokenize("doc -> layers -> annotations").unwrap_or_default();
335        assert_eq!(tokens[0].token, Token::Ident("doc".into()));
336        assert_eq!(tokens[1].token, Token::Arrow);
337        assert_eq!(tokens[2].token, Token::Ident("layers".into()));
338        assert_eq!(tokens[3].token, Token::Arrow);
339        assert_eq!(tokens[4].token, Token::Ident("annotations".into()));
340    }
341}