Skip to main content

lambda_cat/
lexer.rs

1//! Tokenizer for the lambda calculus surface syntax.
2//!
3//! Source must be ASCII; any non-ASCII byte is reported as [`Error::UnexpectedChar`].
4//! Lexing is purely functional: the entry point [`lex`] threads an immutable
5//! accumulator through a recursive scan, producing a `Vec<Token>` on success.
6//!
7//! [`Error::UnexpectedChar`]: crate::error::Error::UnexpectedChar
8
9use crate::error::Error;
10use crate::syntax::{Position, VarName};
11
12/// A token paired with its source position.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct Token {
15    kind: TokenKind,
16    at: Position,
17}
18
19impl Token {
20    /// The token's syntactic kind.
21    #[must_use]
22    pub fn kind(&self) -> &TokenKind {
23        &self.kind
24    }
25
26    /// Byte offset where the token begins in the source.
27    #[must_use]
28    pub fn at(&self) -> Position {
29        self.at
30    }
31
32    fn new(kind: TokenKind, at: Position) -> Self {
33        Self { kind, at }
34    }
35}
36
37/// The syntactic kind of a token.
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum TokenKind {
40    /// An identifier that is not a reserved keyword.
41    Ident(VarName),
42    /// The `let` keyword.
43    KwLet,
44    /// The `in` keyword.
45    KwIn,
46    /// The `fix` keyword.
47    KwFix,
48    /// A lambda head, written `\`.
49    Lambda,
50    /// A dot `.` separating a lambda head from its body.
51    Dot,
52    /// An equals sign `=` in a let-binding.
53    Equals,
54    /// An opening parenthesis `(`.
55    LParen,
56    /// A closing parenthesis `)`.
57    RParen,
58}
59
60impl std::fmt::Display for TokenKind {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        match self {
63            Self::Ident(name) => write!(f, "identifier {:?}", name.as_str()),
64            Self::KwLet => f.write_str("keyword `let`"),
65            Self::KwIn => f.write_str("keyword `in`"),
66            Self::KwFix => f.write_str("keyword `fix`"),
67            Self::Lambda => f.write_str("`\\`"),
68            Self::Dot => f.write_str("`.`"),
69            Self::Equals => f.write_str("`=`"),
70            Self::LParen => f.write_str("`(`"),
71            Self::RParen => f.write_str("`)`"),
72        }
73    }
74}
75
76/// One step of the lexer: either a byte to dispatch on or end-of-input.
77enum Step {
78    End,
79    Byte(u8),
80}
81
82fn peek(src: &[u8], pos: usize) -> Step {
83    src.get(pos).copied().map_or(Step::End, Step::Byte)
84}
85
86/// Lex the entire source string into a vector of tokens.
87///
88/// # Errors
89///
90/// Returns [`Error::UnexpectedChar`] on any non-ASCII byte or any character
91/// outside the grammar.
92///
93/// # Examples
94///
95/// ```
96/// # fn main() -> Result<(), lambda_cat::error::Error> {
97/// use lambda_cat::lexer::{lex, TokenKind};
98///
99/// let tokens = lex("\\x. x")?;
100/// assert_eq!(tokens.len(), 4);
101/// # Ok(())
102/// # }
103/// ```
104pub fn lex(src: &str) -> Result<Vec<Token>, Error> {
105    step(src.as_bytes(), 0, Vec::new())
106}
107
108fn step(src: &[u8], pos: usize, acc: Vec<Token>) -> Result<Vec<Token>, Error> {
109    match peek(src, pos) {
110        Step::End => Ok(acc),
111        Step::Byte(b) => take_token(src, pos, acc, b),
112    }
113}
114
115fn take_token(src: &[u8], pos: usize, acc: Vec<Token>, b: u8) -> Result<Vec<Token>, Error> {
116    match b {
117        b' ' | b'\t' | b'\n' | b'\r' => step(src, pos + 1, acc),
118        b'\\' => emit(src, pos + 1, acc, Token::new(TokenKind::Lambda, pos.into())),
119        b'.' => emit(src, pos + 1, acc, Token::new(TokenKind::Dot, pos.into())),
120        b'=' => emit(src, pos + 1, acc, Token::new(TokenKind::Equals, pos.into())),
121        b'(' => emit(src, pos + 1, acc, Token::new(TokenKind::LParen, pos.into())),
122        b')' => emit(src, pos + 1, acc, Token::new(TokenKind::RParen, pos.into())),
123        other if is_ident_start(other) => read_ident(src, pos, acc),
124        other => Err(Error::UnexpectedChar {
125            at: pos.into(),
126            ch: char::from(other),
127        }),
128    }
129}
130
131fn emit(src: &[u8], next_pos: usize, acc: Vec<Token>, token: Token) -> Result<Vec<Token>, Error> {
132    step(src, next_pos, push(acc, token))
133}
134
135fn push(acc: Vec<Token>, token: Token) -> Vec<Token> {
136    acc.into_iter().chain(std::iter::once(token)).collect()
137}
138
139fn read_ident(src: &[u8], start: usize, acc: Vec<Token>) -> Result<Vec<Token>, Error> {
140    let end = scan_ident(src, start);
141    let slice = src.get(start..end).unwrap_or(&[]);
142    let token = classify_ident(slice, start);
143    step(src, end, push(acc, token))
144}
145
146fn scan_ident(src: &[u8], pos: usize) -> usize {
147    src.get(pos)
148        .copied()
149        .filter(|b| is_ident_continue(*b))
150        .map_or(pos, |_| scan_ident(src, pos + 1))
151}
152
153fn classify_ident(slice: &[u8], start: usize) -> Token {
154    let at = Position::from(start);
155    match slice {
156        b"let" => Token::new(TokenKind::KwLet, at),
157        b"in" => Token::new(TokenKind::KwIn, at),
158        b"fix" => Token::new(TokenKind::KwFix, at),
159        bytes => Token::new(
160            TokenKind::Ident(VarName::from(
161                std::str::from_utf8(bytes).unwrap_or_default(),
162            )),
163            at,
164        ),
165    }
166}
167
168fn is_ident_start(b: u8) -> bool {
169    b.is_ascii_alphabetic() || b == b'_'
170}
171
172fn is_ident_continue(b: u8) -> bool {
173    b.is_ascii_alphanumeric() || b == b'_'
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn lex_identity_lambda() -> Result<(), Error> {
182        let tokens = lex("\\x. x")?;
183        let kinds: Vec<TokenKind> = tokens.iter().map(|t| t.kind().clone()).collect();
184        let expected = vec![
185            TokenKind::Lambda,
186            TokenKind::Ident(VarName::from("x")),
187            TokenKind::Dot,
188            TokenKind::Ident(VarName::from("x")),
189        ];
190        (kinds == expected)
191            .then_some(())
192            .ok_or(Error::UnexpectedEnd {
193                expected: "identity tokenization",
194            })
195    }
196
197    #[test]
198    fn lex_let_keyword() -> Result<(), Error> {
199        let tokens = lex("let x = y in x")?;
200        let kinds: Vec<TokenKind> = tokens.iter().map(|t| t.kind().clone()).collect();
201        let expected = vec![
202            TokenKind::KwLet,
203            TokenKind::Ident(VarName::from("x")),
204            TokenKind::Equals,
205            TokenKind::Ident(VarName::from("y")),
206            TokenKind::KwIn,
207            TokenKind::Ident(VarName::from("x")),
208        ];
209        (kinds == expected)
210            .then_some(())
211            .ok_or(Error::UnexpectedEnd {
212                expected: "let tokenization",
213            })
214    }
215
216    #[test]
217    fn lex_rejects_non_ascii() -> Result<(), Error> {
218        let result = lex("λx. x");
219        match result {
220            Err(Error::UnexpectedChar { .. }) => Ok(()),
221            Err(other) => Err(other),
222            Ok(_) => Err(Error::UnexpectedEnd {
223                expected: "rejection of non-ASCII source",
224            }),
225        }
226    }
227}