1use std::num::{ParseFloatError, ParseIntError};
26
27use logos::{Lexer, Logos};
28use thiserror::Error;
29
30use crate::span::Span;
31
32#[derive(Debug, Clone, PartialEq)]
33pub enum TokenKind {
34 Shebang(String),
36 LParen,
37 RParen,
38 LBrace,
39 RBrace,
40 LBracket,
41 RBracket,
42 Quote,
43 Quasiquote,
44 Unquote,
45 UnquoteSplice,
46 Str(String),
47 Int(i64),
48 Float(f64),
49 Bool(bool),
50 Nil,
51 Symbol(String),
52 Keyword(String),
53 LineComment(String),
54 Newlines(u32),
55 Whitespace,
56}
57
58#[derive(Debug, Clone, PartialEq)]
59pub struct Token {
60 pub kind: TokenKind,
61 pub span: Span,
62}
63
64#[derive(Debug, Default, Error, PartialEq, Eq, Clone)]
65pub enum LexError {
66 #[default]
67 #[error("unrecognized token")]
68 Unrecognized,
69 #[error("unterminated string at offset {0}")]
70 UnterminatedString(u32),
71 #[error("invalid escape sequence \\{1} at offset {0}")]
72 BadEscape(u32, char),
73 #[error("invalid number literal at offset {0}: {1}")]
74 BadInt(u32, String),
75 #[error("invalid float literal at offset {0}: {1}")]
76 BadFloat(u32, String),
77 #[error("unexpected character {1:?} at offset {0}")]
78 UnexpectedChar(u32, char),
79}
80
81impl From<(u32, ParseIntError)> for LexError {
82 fn from(v: (u32, ParseIntError)) -> Self {
83 Self::BadInt(v.0, v.1.to_string())
84 }
85}
86
87impl From<(u32, ParseFloatError)> for LexError {
88 fn from(v: (u32, ParseFloatError)) -> Self {
89 Self::BadFloat(v.0, v.1.to_string())
90 }
91}
92
93#[derive(Logos, Debug, PartialEq)]
99#[logos(error = LexError)]
100enum LogosKind {
101 #[token("(")]
102 LParen,
103
104 #[token(")")]
105 RParen,
106
107 #[token("{")]
121 LBrace,
122
123 #[token("}")]
124 RBrace,
125
126 #[token("[")]
127 LBracket,
128
129 #[token("]")]
130 RBracket,
131
132 #[token("'")]
133 Quote,
134
135 #[token("`")]
136 Quasiquote,
137
138 #[token(",@")]
140 UnquoteSplice,
141
142 #[token(",")]
143 Unquote,
144
145 #[token("#t", |_| true)]
146 #[token("#f", |_| false)]
147 Bool(bool),
148
149 #[regex(r#""(?:[^"\\]|\\.)*""#, lex_string_body)]
153 Str(String),
154
155 #[regex(r"[+-]?[0-9]+", priority = 3, callback = parse_int)]
158 Int(i64),
159
160 #[regex(
161 r"[+-]?(?:[0-9]+\.[0-9]*|\.[0-9]+|[0-9]+[eE][+-]?[0-9]+|[0-9]+\.[0-9]*[eE][+-]?[0-9]+|\.[0-9]+[eE][+-]?[0-9]+)",
162 priority = 3,
163 callback = parse_float
164 )]
165 Float(f64),
166
167 #[regex(":[^\\s()'`,\";\\{\\}\\[\\]]+", |lex| lex.slice()[1..].to_string())]
170 Keyword(String),
171
172 #[regex(r";[^\n]*", |lex| {
175 let s = lex.slice();
176 s[1..].to_string()
178 })]
179 LineComment(String),
180
181 #[regex(r"[\n][ \t\r\n]*", count_newlines)]
185 Newlines(u32),
186
187 #[regex(r"[ \t\r]+")]
191 Whitespace,
192
193 #[regex(
206 "[^\\s()'`,\";#\\{\\}\\[\\]][^\\s()'`,\";#\\{\\}\\[\\]]*",
207 |lex| lex.slice().to_string()
208 )]
209 Symbol(String),
210}
211
212fn lex_string_body(lex: &mut Lexer<LogosKind>) -> Result<String, LexError> {
215 let raw = lex.slice();
216 debug_assert!(raw.starts_with('"') && raw.ends_with('"'));
217 let inner = &raw[1..raw.len() - 1];
218 let span_start = u32::try_from(lex.span().start).unwrap_or(u32::MAX);
219
220 let mut out = String::with_capacity(inner.len());
221 let mut chars = inner.char_indices();
222 while let Some((i, c)) = chars.next() {
223 if c == '\\' {
224 match chars.next() {
225 Some((_, 'n')) => out.push('\n'),
226 Some((_, 't')) => out.push('\t'),
227 Some((_, 'r')) => out.push('\r'),
228 Some((_, '"')) => out.push('"'),
229 Some((_, '\\')) => out.push('\\'),
230 Some((_, other)) => out.push(other),
244 None => {
245 return Err(LexError::BadEscape(
246 span_start + 1 + u32::try_from(i).unwrap_or(0),
247 '\\',
248 ));
249 }
250 }
251 } else {
252 out.push(c);
253 }
254 }
255 Ok(out)
256}
257
258fn parse_int(lex: &mut Lexer<LogosKind>) -> Result<i64, LexError> {
259 let span_start = u32::try_from(lex.span().start).unwrap_or(u32::MAX);
260 lex.slice()
261 .parse::<i64>()
262 .map_err(|e| LexError::BadInt(span_start, e.to_string()))
263}
264
265fn parse_float(lex: &mut Lexer<LogosKind>) -> Result<f64, LexError> {
266 let span_start = u32::try_from(lex.span().start).unwrap_or(u32::MAX);
267 lex.slice()
268 .parse::<f64>()
269 .map_err(|e| LexError::BadFloat(span_start, e.to_string()))
270}
271
272fn count_newlines(lex: &mut Lexer<LogosKind>) -> u32 {
273 let s = lex.slice();
274 let n = s.bytes().filter(|&b| b == b'\n').count();
275 u32::try_from(n).unwrap_or(u32::MAX)
276}
277
278pub fn tokenize(src: &str) -> Result<Vec<Token>, LexError> {
283 let mut out = Vec::new();
284
285 let body_start = if src.starts_with("#!") {
290 let end = src.find('\n').unwrap_or(src.len());
291 out.push(Token {
292 kind: TokenKind::Shebang(src[..end].to_string()),
293 span: Span::new(0, u32::try_from(end).unwrap_or(u32::MAX)),
294 });
295 end
296 } else {
297 0
298 };
299
300 let mut lex = LogosKind::lexer(&src[body_start..]);
301
302 while let Some(result) = lex.next() {
303 let span = lex.span();
304 let span_start = u32::try_from(span.start + body_start).unwrap_or(u32::MAX);
305 let span_end = u32::try_from(span.end + body_start).unwrap_or(u32::MAX);
306 let span = Span::new(span_start, span_end);
307
308 match result {
309 Ok(kind) => {
310 let public = match kind {
311 LogosKind::LParen => TokenKind::LParen,
312 LogosKind::RParen => TokenKind::RParen,
313 LogosKind::LBrace => TokenKind::LBrace,
314 LogosKind::RBrace => TokenKind::RBrace,
315 LogosKind::LBracket => TokenKind::LBracket,
316 LogosKind::RBracket => TokenKind::RBracket,
317 LogosKind::Quote => TokenKind::Quote,
318 LogosKind::Quasiquote => TokenKind::Quasiquote,
319 LogosKind::Unquote => TokenKind::Unquote,
320 LogosKind::UnquoteSplice => TokenKind::UnquoteSplice,
321 LogosKind::Bool(b) => TokenKind::Bool(b),
322 LogosKind::Str(s) => TokenKind::Str(s),
323 LogosKind::Int(i) => TokenKind::Int(i),
324 LogosKind::Float(f) => TokenKind::Float(f),
325 LogosKind::Keyword(s) => TokenKind::Keyword(s),
326 LogosKind::LineComment(s) => TokenKind::LineComment(s),
327 LogosKind::Newlines(n) => TokenKind::Newlines(n),
328 LogosKind::Whitespace => TokenKind::Whitespace,
329 LogosKind::Symbol(s) => {
330 if s == "nil" {
331 TokenKind::Nil
332 } else {
333 TokenKind::Symbol(s)
334 }
335 }
336 };
337 out.push(Token { kind: public, span });
338 }
339 Err(_) => {
340 let slice = lex.slice();
345 if slice.starts_with('"') {
346 return Err(LexError::UnterminatedString(span_start));
347 }
348 let ch = slice.chars().next().unwrap_or(' ');
349 return Err(LexError::UnexpectedChar(span_start, ch));
350 }
351 }
352 }
353
354 Ok(out)
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360
361 fn kinds(src: &str) -> Vec<TokenKind> {
362 tokenize(src)
363 .unwrap()
364 .into_iter()
365 .map(|t| t.kind)
366 .filter(|k| !matches!(k, TokenKind::Whitespace | TokenKind::Newlines(_)))
367 .collect()
368 }
369
370 #[allow(
378 clippy::approx_constant,
379 reason = "float-literal lex fixture, not a PI approximation"
380 )]
381 #[test]
382 fn basic_atoms() {
383 assert_eq!(kinds("42"), vec![TokenKind::Int(42)]);
384 assert_eq!(kinds("3.14"), vec![TokenKind::Float(3.14)]);
385 assert_eq!(kinds("-7"), vec![TokenKind::Int(-7)]);
386 assert_eq!(kinds("#t"), vec![TokenKind::Bool(true)]);
387 assert_eq!(kinds("#f"), vec![TokenKind::Bool(false)]);
388 assert_eq!(kinds("nil"), vec![TokenKind::Nil]);
389 assert_eq!(kinds("\"hi\\n\""), vec![TokenKind::Str("hi\n".into())]);
390 assert_eq!(
391 kinds(":key-word"),
392 vec![TokenKind::Keyword("key-word".into())]
393 );
394 assert_eq!(kinds("my-sym"), vec![TokenKind::Symbol("my-sym".into())]);
395 }
396
397 #[test]
398 fn lists_and_readers() {
399 assert_eq!(
400 kinds("(a b)"),
401 vec![
402 TokenKind::LParen,
403 TokenKind::Symbol("a".into()),
404 TokenKind::Symbol("b".into()),
405 TokenKind::RParen,
406 ]
407 );
408 assert_eq!(
409 kinds("'x"),
410 vec![TokenKind::Quote, TokenKind::Symbol("x".into())]
411 );
412 assert_eq!(
413 kinds(",@xs"),
414 vec![TokenKind::UnquoteSplice, TokenKind::Symbol("xs".into())]
415 );
416 }
417
418 #[test]
419 fn line_comment() {
420 let toks = tokenize("; hello\nworld").unwrap();
421 assert!(matches!(toks[0].kind, TokenKind::LineComment(ref s) if s == " hello"));
422 assert!(matches!(toks[1].kind, TokenKind::Newlines(_)));
423 assert!(matches!(toks[2].kind, TokenKind::Symbol(ref s) if s == "world"));
424 }
425
426 #[test]
427 fn unterminated_string_errors() {
428 assert!(matches!(
429 tokenize(r#""oops"#),
430 Err(LexError::UnterminatedString(_))
431 ));
432 }
433
434 #[test]
435 fn utf8_in_string_round_trip() {
436 let src = r#""π — émoji 🎉""#;
439 let toks = tokenize(src).unwrap();
440 match &toks[0].kind {
441 TokenKind::Str(s) => assert_eq!(s, "π — émoji 🎉"),
442 other => panic!("{other:?}"),
443 }
444 }
445
446 #[test]
447 fn newline_run_preserves_count() {
448 let toks = tokenize("a\n\n\nb").unwrap();
449 assert!(matches!(toks[0].kind, TokenKind::Symbol(ref s) if s == "a"));
451 match toks[1].kind {
452 TokenKind::Newlines(n) => assert_eq!(n, 3),
453 ref other => panic!("{other:?}"),
454 }
455 assert!(matches!(toks[2].kind, TokenKind::Symbol(ref s) if s == "b"));
456 }
457
458 #[test]
459 fn float_with_exponent() {
460 assert_eq!(kinds("1.5e10"), vec![TokenKind::Float(1.5e10)]);
461 assert_eq!(kinds("1e-3"), vec![TokenKind::Float(1e-3)]);
462 assert_eq!(kinds("-2.5E2"), vec![TokenKind::Float(-2.5e2)]);
463 }
464
465 #[test]
466 fn bool_keyword_clash_handled() {
467 assert_eq!(
470 kinds("#t#f"),
471 vec![TokenKind::Bool(true), TokenKind::Bool(false)]
472 );
473 }
474}