Skip to main content

bubbles/compiler/
lexer.rs

1//! Logos-based lexer that tokenises `.bub` source and expression strings.
2
3use logos::Logos;
4
5/// Resolves the `\"`, `\\`, and `\n` escapes in a string-literal body in a
6/// single pass. Unknown escapes are kept verbatim.
7fn unescape(body: &str) -> String {
8    let mut out = String::with_capacity(body.len());
9    let mut chars = body.chars();
10    while let Some(c) = chars.next() {
11        if c != '\\' {
12            out.push(c);
13            continue;
14        }
15        match chars.next() {
16            Some('"') => out.push('"'),
17            Some('n') => out.push('\n'),
18            Some('\\') | None => out.push('\\'),
19            Some(other) => {
20                out.push('\\');
21                out.push(other);
22            }
23        }
24    }
25    out
26}
27
28/// A lexical token produced by the lexer.
29#[derive(Logos, Debug, Clone, PartialEq)]
30#[logos(skip r"[ \t\r\f]+")] // skip horizontal whitespace; newlines are significant in the parser
31pub enum Token {
32    // ── literals ──────────────────────────────────────────────────────────────
33    /// Floating-point or integer literal.
34    #[regex(r"[0-9]+(\.[0-9]+)?", |lex| lex.slice().parse::<f64>().ok())]
35    Number(f64),
36
37    /// Double-quoted string literal.
38    #[regex(r#""([^"\\]|\\.)*""#, |lex| {
39        let s = lex.slice();
40        Some(unescape(&s[1..s.len() - 1]))
41    })]
42    Str(String),
43
44    // ── identifiers / keywords ─────────────────────────────────────────────────
45    /// Variable beginning with `$`.
46    #[regex(r"\$[A-Za-z_][A-Za-z0-9_]*", |lex| lex.slice().to_owned())]
47    Var(String),
48
49    /// Plain identifier or keyword.
50    #[regex(r"[A-Za-z_][A-Za-z0-9_]*", |lex| lex.slice().to_owned())]
51    Ident(String),
52
53    // ── delimiters ─────────────────────────────────────────────────────────────
54    /// `(` – opens a parenthesised sub-expression or argument list.
55    #[token("(")]
56    LParen,
57    /// `)` – closes a parenthesised sub-expression or argument list.
58    #[token(")")]
59    RParen,
60    /// `,` – argument separator.
61    #[token(",")]
62    Comma,
63    /// `<<` – opens a command/statement block.
64    #[token("<<")]
65    CmdOpen,
66    /// `>>` – closes a command/statement block.
67    #[token(">>")]
68    CmdClose,
69    /// `{` – opens an inline expression.
70    #[token("{")]
71    BraceOpen,
72    /// `}` – closes an inline expression.
73    #[token("}")]
74    BraceClose,
75
76    // ── arithmetic ─────────────────────────────────────────────────────────────
77    /// `+`
78    #[token("+")]
79    Plus,
80    /// `-`
81    #[token("-")]
82    Minus,
83    /// `*`
84    #[token("*")]
85    Star,
86    /// `/`
87    #[token("/")]
88    Slash,
89    /// `%`
90    #[token("%")]
91    Percent,
92
93    // ── comparison (order matters: `>=` before `>`) ───────────────────────────
94    /// `>=`
95    #[token(">=")]
96    Gte,
97    /// `<=`
98    #[token("<=")]
99    Lte,
100    /// `>`
101    #[token(">")]
102    Gt,
103    /// `<`
104    #[token("<")]
105    Lt,
106    /// `==`
107    #[token("==")]
108    EqEq,
109    /// `!=`
110    #[token("!=")]
111    Neq,
112
113    // ── logical ────────────────────────────────────────────────────────────────
114    /// `&&`
115    #[token("&&")]
116    AndAnd,
117    /// `||`
118    #[token("||")]
119    OrOr,
120    /// `!`
121    #[token("!")]
122    Bang,
123
124    // ── assignment / misc ──────────────────────────────────────────────────────
125    /// `=` (used in `<<set $x = …>>`)
126    #[token("=")]
127    Eq,
128    /// `:`
129    #[token(":")]
130    Colon,
131    /// `->`
132    #[token("->")]
133    Arrow,
134    /// `=>`
135    #[token("=>")]
136    FatArrow,
137    /// `---` body-start delimiter.
138    #[token("---")]
139    BodyStart,
140    /// `===` node-end delimiter.
141    #[token("===")]
142    NodeEnd,
143    /// `#` tag prefix.
144    #[token("#")]
145    Hash,
146    /// Newline.
147    #[token("\n")]
148    Newline,
149}
150
151/// A spanned token pair.
152pub type Spanned = (Token, std::ops::Range<usize>);
153
154/// Lexes `input` into a [`Vec`] of spanned tokens, returning an error on
155/// any character that does not match a known token.
156///
157/// # Errors
158///
159/// Returns [`crate::error::DialogueError::Parse`] with `file` / `line` context
160/// when an unrecognised character is encountered, so the caller receives a
161/// precise pointer into the source rather than a confusing downstream failure.
162pub fn tokenise(input: &str, file: &str, line: usize) -> crate::error::Result<Vec<Spanned>> {
163    let mut tokens = Vec::new();
164    for (result, span) in Token::lexer(input).spanned() {
165        if let Ok(tok) = result {
166            tokens.push((tok, span));
167        } else {
168            let ch = input[span].chars().next().unwrap_or('?');
169            return Err(crate::error::DialogueError::Parse {
170                file: file.to_owned(),
171                line,
172                message: format!(
173                    "unexpected character `{ch}` in expression; \
174                     did you mean `$` for a variable?"
175                ),
176            });
177        }
178    }
179    Ok(tokens)
180}
181
182#[cfg(test)]
183#[path = "lexer_tests.rs"]
184mod tests;