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