1use std::fmt;
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub struct Span {
27 pub start: usize,
28 pub end: usize,
29}
30
31impl Span {
32 pub fn new(start: usize, end: usize) -> Self {
33 Self { start, end }
34 }
35}
36
37#[derive(Clone, Debug, PartialEq)]
38pub enum TokenKind {
39 Int(i64),
41 Float(f64),
42 Str(String),
43 InterpolatedStr {
51 parts: Vec<String>,
55 exprs: Vec<String>,
56 },
57 Sym(String),
59 True,
60 False,
61 Nil,
62
63 Ident(String),
66
67 LParen,
69 RParen,
70 LBracket,
71 RBracket,
72 LBrace,
73 RBrace,
74 Comma,
75 Dot,
76 Colon,
79 Label(String),
83 Rocket,
85 Pipe,
87
88 Op(String),
90
91 Comment(String),
93 Newline,
94
95 Eof,
96}
97
98#[derive(Clone, Debug, PartialEq)]
99pub struct Token {
100 pub kind: TokenKind,
101 pub span: Span,
102}
103
104impl Token {
105 pub fn is_trivia(&self) -> bool {
107 matches!(self.kind, TokenKind::Comment(_) | TokenKind::Newline)
108 }
109}
110
111#[derive(Clone, Debug, PartialEq)]
112pub struct LexError {
113 pub message: String,
114 pub span: Span,
115}
116
117impl fmt::Display for LexError {
118 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119 write!(f, "{} at {}..{}", self.message, self.span.start, self.span.end)
120 }
121}
122
123impl std::error::Error for LexError {}
124
125const OP_CHARS: &str = "+-*/=<>!%&|";
127
128pub fn lex(src: &str) -> Result<Vec<Token>, LexError> {
130 Lexer::new(src).run()
131}
132
133struct Lexer<'a> {
134 src: &'a str,
135 bytes: &'a [u8],
136 pos: usize,
137 out: Vec<Token>,
138}
139
140impl<'a> Lexer<'a> {
141 fn new(src: &'a str) -> Self {
142 Self {
143 src,
144 bytes: src.as_bytes(),
145 pos: 0,
146 out: Vec::new(),
147 }
148 }
149
150 fn peek(&self) -> Option<u8> {
151 self.bytes.get(self.pos).copied()
152 }
153
154 fn peek_at(&self, n: usize) -> Option<u8> {
155 self.bytes.get(self.pos + n).copied()
156 }
157
158 fn push(&mut self, kind: TokenKind, start: usize) {
159 self.out.push(Token {
160 kind,
161 span: Span::new(start, self.pos),
162 });
163 }
164
165 fn err(&self, message: impl Into<String>, start: usize) -> LexError {
166 LexError {
167 message: message.into(),
168 span: Span::new(start, self.pos.max(start + 1)),
169 }
170 }
171
172 fn run(mut self) -> Result<Vec<Token>, LexError> {
173 while let Some(c) = self.peek() {
174 let start = self.pos;
175 match c {
176 b'\n' => {
177 self.pos += 1;
178 self.push(TokenKind::Newline, start);
179 }
180 b' ' | b'\t' | b'\r' => {
184 self.pos += 1;
185 }
186 b'#' => {
187 while let Some(c) = self.peek() {
188 if c == b'\n' {
189 break;
190 }
191 self.pos += 1;
192 }
193 let text = self.src[start..self.pos].to_string();
194 self.push(TokenKind::Comment(text), start);
195 }
196 b'"' => self.lex_string(start)?,
197 b'0'..=b'9' => self.lex_number(start)?,
198 b':' => self.lex_colon(start),
199 b'(' => self.one(TokenKind::LParen, start),
200 b')' => self.one(TokenKind::RParen, start),
201 b'[' => self.one(TokenKind::LBracket, start),
202 b']' => self.one(TokenKind::RBracket, start),
203 b'{' => self.one(TokenKind::LBrace, start),
204 b'}' => self.one(TokenKind::RBrace, start),
205 b',' => self.one(TokenKind::Comma, start),
206 b'.' => self.one(TokenKind::Dot, start),
207 c if is_ident_start(c) => self.lex_ident(start),
208 c if OP_CHARS.as_bytes().contains(&c) => self.lex_op(start),
209 _ => {
210 self.pos += 1;
211 return Err(self.err(
212 format!("unexpected character {:?}", c as char),
213 start,
214 ));
215 }
216 }
217 }
218 let end = self.pos;
219 self.out.push(Token {
220 kind: TokenKind::Eof,
221 span: Span::new(end, end),
222 });
223 Ok(self.out)
224 }
225
226 fn one(&mut self, kind: TokenKind, start: usize) {
227 self.pos += 1;
228 self.push(kind, start);
229 }
230
231 fn lex_string(&mut self, start: usize) -> Result<(), LexError> {
232 self.pos += 1; let mut buf = String::new();
234 let mut parts: Vec<String> = Vec::new();
237 let mut exprs: Vec<String> = Vec::new();
238 loop {
239 if self.peek() == Some(b'#') && self.src.as_bytes().get(self.pos + 1) == Some(&b'{') {
242 self.pos += 2;
243 let expr_start = self.pos;
244 let mut depth = 1usize;
246 while let Some(c) = self.peek() {
247 match c {
248 b'{' => depth += 1,
249 b'}' => {
250 depth -= 1;
251 if depth == 0 {
252 break;
253 }
254 }
255 _ => {}
256 }
257 self.pos += 1;
258 }
259 if self.peek() != Some(b'}') {
260 return Err(self.err("unterminated `#{` interpolation", start));
261 }
262 exprs.push(self.src[expr_start..self.pos].to_string());
263 self.pos += 1; parts.push(std::mem::take(&mut buf));
265 continue;
266 }
267 match self.peek() {
268 None => return Err(self.err("unterminated string literal", start)),
269 Some(b'"') => {
270 self.pos += 1;
271 break;
272 }
273 Some(b'\\') => {
274 self.pos += 1;
275 let esc = self
276 .peek()
277 .ok_or_else(|| self.err("unterminated escape", start))?;
278 let ch = match esc {
279 b'n' => '\n',
280 b't' => '\t',
281 b'r' => '\r',
282 b'\\' => '\\',
283 b'"' => '"',
284 b'0' => '\0',
285 b'u' => {
292 self.pos += 1; if self.peek() != Some(b'{') {
294 return Err(self.err("expected `{` after \\u", start));
295 }
296 self.pos += 1; let hex_start = self.pos;
298 while self.peek().is_some_and(|c| c != b'}') {
299 self.pos += 1;
300 }
301 if self.peek() != Some(b'}') {
302 return Err(self.err("unterminated \\u{...} escape", start));
303 }
304 let hex = &self.src[hex_start..self.pos];
305 let code = u32::from_str_radix(hex, 16).map_err(|_| {
306 self.err(format!("`{hex}` is not hexadecimal"), start)
307 })?;
308 let ch = char::from_u32(code).ok_or_else(|| {
313 self.err(
314 format!("`{hex}` is not a Unicode scalar value"),
315 start,
316 )
317 })?;
318 buf.push(ch);
319 self.pos += 1; continue;
321 }
322 other => {
323 return Err(self.err(
324 format!("unknown escape \\{}", other as char),
325 start,
326 ))
327 }
328 };
329 buf.push(ch);
330 self.pos += 1;
331 }
332 Some(_) => {
333 let ch = self.src[self.pos..]
334 .chars()
335 .next()
336 .expect("peek said there is a byte");
337 buf.push(ch);
338 self.pos += ch.len_utf8();
339 }
340 }
341 }
342 if exprs.is_empty() {
343 self.push(TokenKind::Str(buf), start);
344 } else {
345 parts.push(buf);
346 self.push(TokenKind::InterpolatedStr { parts, exprs }, start);
347 }
348 Ok(())
349 }
350
351 fn lex_number(&mut self, start: usize) -> Result<(), LexError> {
352 while matches!(self.peek(), Some(b'0'..=b'9' | b'_')) {
353 self.pos += 1;
354 }
355 let is_float = self.peek() == Some(b'.')
359 && matches!(self.peek_at(1), Some(b'0'..=b'9'));
360 if is_float {
361 self.pos += 1;
362 while matches!(self.peek(), Some(b'0'..=b'9' | b'_')) {
363 self.pos += 1;
364 }
365 }
366 let text: String = self.src[start..self.pos].chars().filter(|c| *c != '_').collect();
367 if is_float {
368 let v: f64 = text
369 .parse()
370 .map_err(|_| self.err(format!("invalid float literal {text:?}"), start))?;
371 self.push(TokenKind::Float(v), start);
372 } else {
373 let v: i64 = text
374 .parse()
375 .map_err(|_| self.err(format!("integer literal out of range: {text:?}"), start))?;
376 self.push(TokenKind::Int(v), start);
377 }
378 Ok(())
379 }
380
381 fn lex_colon(&mut self, start: usize) {
382 if matches!(self.peek_at(1), Some(c) if is_ident_start(c)) {
384 self.pos += 1;
385 let s = self.pos;
386 while matches!(self.peek(), Some(c) if is_ident_continue(c)) {
387 self.pos += 1;
388 }
389 let name = self.src[s..self.pos].to_string();
390 self.push(TokenKind::Sym(name), start);
391 } else {
392 self.one(TokenKind::Colon, start);
393 }
394 }
395
396 fn lex_ident(&mut self, start: usize) {
397 while matches!(self.peek(), Some(c) if is_ident_continue(c)) {
398 self.pos += 1;
399 }
400 if matches!(self.peek(), Some(b'?') | Some(b'!')) {
402 self.pos += 1;
403 }
404 let name = self.src[start..self.pos].to_string();
405
406 if self.peek() == Some(b':') && self.peek_at(1) != Some(b':') {
410 self.pos += 1;
411 self.push(TokenKind::Label(name), start);
412 return;
413 }
414
415 let kind = match name.as_str() {
416 "true" => TokenKind::True,
417 "false" => TokenKind::False,
418 "nil" => TokenKind::Nil,
419 _ => TokenKind::Ident(name),
420 };
421 self.push(kind, start);
422 }
423
424 fn lex_op(&mut self, start: usize) {
425 while matches!(self.peek(), Some(c) if OP_CHARS.as_bytes().contains(&c)) {
426 self.pos += 1;
427 }
428 let text = self.src[start..self.pos].to_string();
429 let kind = match text.as_str() {
430 "=>" => TokenKind::Rocket,
431 "|>" => TokenKind::Pipe,
432 _ => TokenKind::Op(text),
433 };
434 self.push(kind, start);
435 }
436}
437
438fn is_ident_start(c: u8) -> bool {
439 c.is_ascii_alphabetic() || c == b'_'
440}
441
442fn is_ident_continue(c: u8) -> bool {
443 c.is_ascii_alphanumeric() || c == b'_'
444}
445
446#[cfg(test)]
447mod tests {
448 use super::*;
449
450 fn kinds(src: &str) -> Vec<TokenKind> {
451 lex(src)
452 .expect("lex")
453 .into_iter()
454 .filter(|t| !t.is_trivia() && t.kind != TokenKind::Eof)
455 .map(|t| t.kind)
456 .collect()
457 }
458
459 #[test]
460 fn lexes_integers_and_floats() {
461 assert_eq!(kinds("1 2.5 1_000"), vec![
462 TokenKind::Int(1),
463 TokenKind::Float(2.5),
464 TokenKind::Int(1000),
465 ]);
466 }
467
468 #[test]
471 fn a_dot_after_a_digit_is_a_send_unless_a_digit_follows() {
472 assert_eq!(kinds("1.foo"), vec![
473 TokenKind::Int(1),
474 TokenKind::Dot,
475 TokenKind::Ident("foo".into()),
476 ]);
477 }
478
479 #[test]
480 fn lexes_symbols_and_labels_distinctly() {
481 assert_eq!(kinds(":foo"), vec![TokenKind::Sym("foo".into())]);
482 assert_eq!(kinds("foo:"), vec![TokenKind::Label("foo".into())]);
483 }
484
485 #[test]
486 fn ruby_predicate_and_bang_suffixes_are_part_of_the_name() {
487 assert_eq!(kinds("empty? save!"), vec![
488 TokenKind::Ident("empty?".into()),
489 TokenKind::Ident("save!".into()),
490 ]);
491 }
492
493 #[test]
494 fn lexes_strings_with_escapes() {
495 assert_eq!(kinds(r#""a\nb""#), vec![TokenKind::Str("a\nb".into())]);
496 }
497
498 #[test]
499 fn unterminated_string_is_an_error_with_a_span() {
500 let e = lex("\"oops").expect_err("must fail");
501 assert!(e.message.contains("unterminated"), "{}", e.message);
502 assert_eq!(e.span.start, 0);
503 }
504
505 #[test]
508 fn comments_and_newlines_are_emitted_as_trivia() {
509 let toks = lex("1 # hi\n2").expect("lex");
510 assert!(
511 toks.iter().any(|t| matches!(&t.kind, TokenKind::Comment(c) if c == "# hi")),
512 "comment was dropped: {toks:?}"
513 );
514 assert!(
515 toks.iter().any(|t| t.kind == TokenKind::Newline),
516 "newline was dropped"
517 );
518 }
519
520 #[test]
523 fn spans_point_at_the_actual_bytes() {
524 let src = "foo + 1";
525 let toks = lex(src).expect("lex");
526 let first = &toks[0];
527 assert_eq!(&src[first.span.start..first.span.end], "foo");
528 let last_int = toks
529 .iter()
530 .find(|t| matches!(t.kind, TokenKind::Int(_)))
531 .expect("an int token");
532 assert_eq!(&src[last_int.span.start..last_int.span.end], "1");
533 }
534
535 #[test]
536 fn lexes_pipeline_and_rocket() {
537 assert_eq!(kinds("|> =>"), vec![TokenKind::Pipe, TokenKind::Rocket]);
538 }
539}