lexer_lang/cursor.rs
1//! The [`Cursor`]: a zero-copy scanner over source text.
2
3use core::str::Chars;
4
5use intern_lang::{Interner, Symbol};
6use source_lang::SourceFile;
7use span_lang::{BytePos, Span};
8use token_lang::Token;
9
10/// A zero-copy cursor over a `&str`, the primitive every lexer is built on.
11///
12/// A `Cursor` walks source text one [`char`] at a time, tracking a byte position
13/// and the start of the token currently being scanned. A lexer — hand-written or
14/// generated — drives it with the peek/advance primitives ([`first`](Cursor::first),
15/// [`second`](Cursor::second), [`bump`](Cursor::bump), [`bump_if`](Cursor::bump_if),
16/// [`eat_while`](Cursor::eat_while)), then turns the scanned run into a
17/// [`Token<K>`](Token) with [`emit`](Cursor::emit). token-lang says what a token is;
18/// the cursor says where one ends.
19///
20/// It is zero-copy and allocation-free: it borrows the source for its whole life,
21/// reports positions as [`BytePos`] and runs as [`Span`], and hands back lexemes as
22/// borrowed `&str` slices. Advancing is `O(1)`; nothing here allocates.
23///
24/// # Positions
25///
26/// Positions are reported in a *global* space offset by a base (`0` by default).
27/// Construct with [`for_source`](Cursor::for_source) to lex a [`SourceFile`] from a
28/// `source-lang` map, and the spans land in that map's global position space — the
29/// same space a `diag-lang` label points with. Use [`with_base`](Cursor::with_base)
30/// to set the base directly, or [`new`](Cursor::new) for a standalone `&str` at base
31/// `0`.
32///
33/// # Examples
34///
35/// A whole, if tiny, lexer: identifiers, `+`, and whitespace, terminated by EOF.
36///
37/// ```
38/// use intern_lang::Interner;
39/// use token_lang::{Symbol, Token, TokenKind};
40/// use lexer_lang::Cursor;
41///
42/// #[derive(Clone, Copy, Debug, PartialEq, Eq)]
43/// enum Kind {
44/// Ident(Symbol),
45/// Plus,
46/// Whitespace,
47/// Eof,
48/// }
49/// impl TokenKind for Kind {
50/// fn is_trivia(&self) -> bool { matches!(self, Kind::Whitespace) }
51/// fn is_eof(&self) -> bool { matches!(self, Kind::Eof) }
52/// fn symbol(&self) -> Option<Symbol> {
53/// match self { Kind::Ident(s) => Some(*s), _ => None }
54/// }
55/// }
56///
57/// fn lex(src: &str, interner: &mut Interner) -> Vec<Token<Kind>> {
58/// let mut cursor = Cursor::new(src);
59/// let mut tokens = Vec::new();
60/// while let Some(c) = cursor.first() {
61/// let kind = if c.is_whitespace() {
62/// cursor.eat_while(char::is_whitespace);
63/// Kind::Whitespace
64/// } else if c == '+' {
65/// cursor.bump();
66/// Kind::Plus
67/// } else {
68/// cursor.eat_while(|c| !c.is_whitespace() && c != '+');
69/// Kind::Ident(cursor.intern_lexeme(interner))
70/// };
71/// tokens.push(cursor.emit(kind));
72/// }
73/// tokens.push(cursor.emit(Kind::Eof));
74/// tokens
75/// }
76///
77/// let mut interner = Interner::new();
78/// let tokens = lex("foo + bar", &mut interner);
79/// let significant = tokens.iter().filter(|t| !t.is_trivia() && !t.is_eof()).count();
80/// assert_eq!(significant, 3); // foo, +, bar
81/// assert!(tokens.last().unwrap().is_eof());
82/// ```
83#[derive(Clone, Debug)]
84pub struct Cursor<'a> {
85 /// The full source text, kept for slicing lexemes back out.
86 text: &'a str,
87 /// The unconsumed tail; its length gives the current offset.
88 chars: Chars<'a>,
89 /// Local byte offset where the in-progress token started.
90 token_start: u32,
91 /// Offset added to every reported position, placing spans in a global space.
92 base: u32,
93}
94
95impl<'a> Cursor<'a> {
96 /// Creates a cursor over `text`, reporting positions from base `0`.
97 ///
98 /// # Examples
99 ///
100 /// ```
101 /// use lexer_lang::Cursor;
102 ///
103 /// let mut cursor = Cursor::new("ab");
104 /// assert_eq!(cursor.bump(), Some('a'));
105 /// assert_eq!(cursor.bump(), Some('b'));
106 /// assert!(cursor.is_eof());
107 /// ```
108 #[inline]
109 #[must_use]
110 pub fn new(text: &'a str) -> Self {
111 Self::with_base(text, 0)
112 }
113
114 /// Creates a cursor over `text` whose reported positions are offset by `base`.
115 ///
116 /// Use this to lex a slice of a larger source while keeping spans in the larger
117 /// source's coordinate space. [`for_source`](Cursor::for_source) is the usual
118 /// way to get the base right for a `source-lang` file.
119 ///
120 /// `base + text.len()` is expected to fit in `u32` — the addressable envelope the
121 /// `span-lang` / `source-lang` layers cap at. Position arithmetic saturates rather
122 /// than wrapping if it does not, so a span is never off by `2^32`.
123 ///
124 /// # Examples
125 ///
126 /// ```
127 /// use lexer_lang::{BytePos, Cursor};
128 ///
129 /// let mut cursor = Cursor::with_base("xy", 100);
130 /// assert_eq!(cursor.pos(), BytePos::new(100));
131 /// cursor.bump();
132 /// assert_eq!(cursor.pos(), BytePos::new(101));
133 /// ```
134 #[inline]
135 #[must_use]
136 pub fn with_base(text: &'a str, base: u32) -> Self {
137 Self {
138 text,
139 chars: text.chars(),
140 token_start: 0,
141 base,
142 }
143 }
144
145 /// Creates a cursor over a [`SourceFile`]'s text, with its base set so spans
146 /// land in the owning [`SourceMap`](source_lang::SourceMap)'s global position
147 /// space.
148 ///
149 /// This is the bridge from `source-lang` to the lexer: the spans the cursor
150 /// emits resolve directly against the same map a diagnostic renders through, so
151 /// a token's span points at the right file without any further arithmetic.
152 ///
153 /// # Examples
154 ///
155 /// ```
156 /// use source_lang::SourceMap;
157 /// use lexer_lang::Cursor;
158 ///
159 /// let mut map = SourceMap::new();
160 /// map.add("a.txt", "first").expect("fits"); // 0..5
161 /// let id = map.add("b.txt", "x").expect("fits"); // 5..6
162 ///
163 /// let file = map.source(id).unwrap();
164 /// let mut cursor = Cursor::for_source(file);
165 /// // The single token's span is in the map's global space, not 0-based.
166 /// assert_eq!(cursor.pos().to_u32(), 5);
167 /// cursor.bump();
168 /// assert_eq!(cursor.token_span().start().to_u32(), 5);
169 /// ```
170 #[inline]
171 #[must_use]
172 pub fn for_source(file: &'a SourceFile) -> Self {
173 Self::with_base(file.text(), file.span().start().to_u32())
174 }
175
176 /// The unconsumed source text.
177 ///
178 /// # Examples
179 ///
180 /// ```
181 /// use lexer_lang::Cursor;
182 ///
183 /// let mut cursor = Cursor::new("abc");
184 /// cursor.bump();
185 /// assert_eq!(cursor.remaining(), "bc");
186 /// ```
187 #[inline]
188 #[must_use]
189 pub fn remaining(&self) -> &'a str {
190 self.chars.as_str()
191 }
192
193 /// Whether the cursor has reached the end of the source.
194 #[inline]
195 #[must_use]
196 pub fn is_eof(&self) -> bool {
197 self.chars.as_str().is_empty()
198 }
199
200 /// The current local byte offset: how far into `text` the cursor has advanced,
201 /// ignoring the base.
202 #[inline]
203 fn offset(&self) -> u32 {
204 // `text` is the full source and `chars.as_str()` its unconsumed tail, so the
205 // difference is the consumed prefix length — always within `text.len()`,
206 // which the source layers cap at `u32::MAX`.
207 (self.text.len() - self.chars.as_str().len()) as u32
208 }
209
210 /// The current position, in the global space set by the base.
211 ///
212 /// The base plus the source length must fit in `u32` (the addressable envelope
213 /// the source layers cap at); the addition saturates rather than wrapping if it
214 /// somehow does not, so a position is never silently wrong by `2^32`.
215 ///
216 /// # Examples
217 ///
218 /// ```
219 /// use lexer_lang::{BytePos, Cursor};
220 ///
221 /// let mut cursor = Cursor::new("hi");
222 /// assert_eq!(cursor.pos(), BytePos::new(0));
223 /// cursor.bump();
224 /// assert_eq!(cursor.pos(), BytePos::new(1));
225 /// ```
226 #[inline]
227 #[must_use]
228 pub fn pos(&self) -> BytePos {
229 BytePos::new(self.base.saturating_add(self.offset()))
230 }
231
232 /// Peeks the next character without consuming it, or `None` at end of input.
233 ///
234 /// # Examples
235 ///
236 /// ```
237 /// use lexer_lang::Cursor;
238 ///
239 /// let cursor = Cursor::new("xy");
240 /// assert_eq!(cursor.first(), Some('x'));
241 /// ```
242 #[inline]
243 #[must_use]
244 pub fn first(&self) -> Option<char> {
245 self.chars.clone().next()
246 }
247
248 /// Peeks the character after [`first`](Cursor::first) without consuming
249 /// anything — the second character of lookahead, for tokens like `==`, `//`, or
250 /// `..`.
251 ///
252 /// # Examples
253 ///
254 /// ```
255 /// use lexer_lang::Cursor;
256 ///
257 /// let cursor = Cursor::new("==");
258 /// assert_eq!(cursor.first(), Some('='));
259 /// assert_eq!(cursor.second(), Some('='));
260 /// ```
261 #[inline]
262 #[must_use]
263 pub fn second(&self) -> Option<char> {
264 let mut chars = self.chars.clone();
265 let _ = chars.next();
266 chars.next()
267 }
268
269 /// Consumes and returns the next character, or `None` at end of input.
270 ///
271 /// # Examples
272 ///
273 /// ```
274 /// use lexer_lang::Cursor;
275 ///
276 /// let mut cursor = Cursor::new("ab");
277 /// assert_eq!(cursor.bump(), Some('a'));
278 /// assert_eq!(cursor.bump(), Some('b'));
279 /// assert_eq!(cursor.bump(), None);
280 /// ```
281 #[inline]
282 pub fn bump(&mut self) -> Option<char> {
283 self.chars.next()
284 }
285
286 /// Consumes the next character only if it equals `expected`, returning whether
287 /// it did. Handy for two-character operators after the first is known.
288 ///
289 /// # Examples
290 ///
291 /// ```
292 /// use lexer_lang::Cursor;
293 ///
294 /// let mut cursor = Cursor::new("=>");
295 /// assert!(cursor.bump_if('='));
296 /// assert!(cursor.bump_if('>'));
297 /// assert!(!cursor.bump_if('!')); // no match: nothing consumed
298 /// assert!(cursor.is_eof());
299 /// ```
300 #[inline]
301 pub fn bump_if(&mut self, expected: char) -> bool {
302 if self.first() == Some(expected) {
303 let _ = self.bump();
304 true
305 } else {
306 false
307 }
308 }
309
310 /// Consumes characters while `pred` holds.
311 ///
312 /// Stops at the first character for which `pred` is `false`, or at end of input,
313 /// leaving that character unconsumed. This is the workhorse for scanning runs —
314 /// identifier bodies, digit sequences, whitespace. Read what was consumed back
315 /// with [`lexeme`](Cursor::lexeme); it returns nothing so a side-effect-only call
316 /// stays clean under `unused_results`.
317 ///
318 /// # Examples
319 ///
320 /// ```
321 /// use lexer_lang::Cursor;
322 ///
323 /// let mut cursor = Cursor::new("abc123");
324 /// cursor.eat_while(|c| c.is_ascii_alphabetic());
325 /// assert_eq!(cursor.lexeme(), "abc");
326 /// assert_eq!(cursor.remaining(), "123");
327 /// ```
328 #[inline]
329 pub fn eat_while(&mut self, mut pred: impl FnMut(char) -> bool) {
330 while let Some(c) = self.first() {
331 if !pred(c) {
332 break;
333 }
334 let _ = self.bump();
335 }
336 }
337
338 /// The lexeme of the in-progress token: the source text consumed since the last
339 /// [`emit`](Cursor::emit) (or since construction).
340 ///
341 /// # Examples
342 ///
343 /// ```
344 /// use lexer_lang::Cursor;
345 ///
346 /// let mut cursor = Cursor::new("let x");
347 /// cursor.eat_while(|c| c.is_ascii_alphabetic());
348 /// assert_eq!(cursor.lexeme(), "let");
349 /// ```
350 #[inline]
351 #[must_use]
352 pub fn lexeme(&self) -> &'a str {
353 &self.text[self.token_start as usize..self.offset() as usize]
354 }
355
356 /// The [`Span`] of the in-progress token, in the global space set by the base:
357 /// from where the last [`emit`](Cursor::emit) left off to the current position.
358 ///
359 /// # Examples
360 ///
361 /// ```
362 /// use lexer_lang::{Cursor, Span};
363 ///
364 /// let mut cursor = Cursor::with_base("let", 10);
365 /// cursor.eat_while(|c| c.is_ascii_alphabetic());
366 /// assert_eq!(cursor.token_span(), Span::new(10, 13));
367 /// ```
368 #[inline]
369 #[must_use]
370 pub fn token_span(&self) -> Span {
371 Span::new(
372 self.base.saturating_add(self.token_start),
373 self.base.saturating_add(self.offset()),
374 )
375 }
376
377 /// Interns the current [`lexeme`](Cursor::lexeme) into `interner`, returning its
378 /// [`Symbol`] — the cheap handle an identifier or keyword token carries.
379 ///
380 /// # Examples
381 ///
382 /// ```
383 /// use intern_lang::Interner;
384 /// use lexer_lang::Cursor;
385 ///
386 /// let mut interner = Interner::new();
387 /// let mut cursor = Cursor::new("name rest");
388 /// cursor.eat_while(|c| c.is_ascii_alphabetic());
389 /// let sym = cursor.intern_lexeme(&mut interner);
390 /// assert_eq!(interner.resolve(sym), Some("name"));
391 /// ```
392 #[inline]
393 pub fn intern_lexeme(&self, interner: &mut Interner) -> Symbol {
394 interner.intern(self.lexeme())
395 }
396
397 /// Ends the in-progress token, returning a [`Token<K>`](Token) of `kind` spanning
398 /// the consumed run, and starts the next token at the current position.
399 ///
400 /// After `emit`, [`lexeme`](Cursor::lexeme) is empty and
401 /// [`token_span`](Cursor::token_span) is a zero-width span at the current
402 /// position, until more is consumed. Emitting without having consumed anything —
403 /// at end of input, say — yields a token with an empty span, the natural shape
404 /// for an end-of-input marker.
405 ///
406 /// # Examples
407 ///
408 /// ```
409 /// use lexer_lang::{Cursor, Span, Token};
410 ///
411 /// let mut cursor = Cursor::new("ab");
412 /// cursor.bump();
413 /// let first = cursor.emit("a");
414 /// assert_eq!(first, Token::new("a", Span::new(0, 1)));
415 ///
416 /// // The next token starts where the last one ended.
417 /// cursor.bump();
418 /// let second = cursor.emit("b");
419 /// assert_eq!(second, Token::new("b", Span::new(1, 2)));
420 /// ```
421 #[inline]
422 pub fn emit<K>(&mut self, kind: K) -> Token<K> {
423 let token = Token::new(kind, self.token_span());
424 self.token_start = self.offset();
425 token
426 }
427
428 /// Discards the in-progress run without producing a token, starting the next
429 /// token at the current position.
430 ///
431 /// This is the counterpart to [`emit`](Cursor::emit) for trivia a lexer drops
432 /// rather than keeps: consume the whitespace or comment, then `reset_token` so it
433 /// is not folded into the span of the token that follows. (A lexer that instead
434 /// *emits* trivia — for a formatter or a lossless tree — uses `emit` with a
435 /// trivia kind and never needs this.)
436 ///
437 /// # Examples
438 ///
439 /// ```
440 /// use lexer_lang::{Cursor, Span};
441 ///
442 /// let mut cursor = Cursor::new(" x");
443 /// cursor.eat_while(char::is_whitespace);
444 /// cursor.reset_token(); // drop the leading spaces
445 ///
446 /// cursor.eat_while(|c| c.is_ascii_alphabetic());
447 /// // The identifier's span covers only `x`, not the spaces before it.
448 /// assert_eq!(cursor.token_span(), Span::new(2, 3));
449 /// assert_eq!(cursor.lexeme(), "x");
450 /// ```
451 #[inline]
452 pub fn reset_token(&mut self) {
453 self.token_start = self.offset();
454 }
455}
456
457#[cfg(test)]
458mod tests {
459 // Tests assert on known-present peeks and just-added sources; unwrapping them
460 // is the clearest form and is confined to test code. `bump`/`add` are also
461 // called for their side effect, so their results are intentionally dropped.
462 #![allow(clippy::unwrap_used, clippy::expect_used, unused_results)]
463
464 extern crate alloc;
465 use alloc::string::String;
466 use alloc::vec::Vec;
467
468 use super::*;
469
470 #[test]
471 fn test_bump_walks_every_char_then_eof() {
472 let mut cursor = Cursor::new("abc");
473 assert_eq!(cursor.bump(), Some('a'));
474 assert_eq!(cursor.bump(), Some('b'));
475 assert_eq!(cursor.bump(), Some('c'));
476 assert_eq!(cursor.bump(), None);
477 assert!(cursor.is_eof());
478 }
479
480 #[test]
481 fn test_first_and_second_peek_without_consuming() {
482 let cursor = Cursor::new("=>x");
483 assert_eq!(cursor.first(), Some('='));
484 assert_eq!(cursor.second(), Some('>'));
485 // Peeking did not advance.
486 assert_eq!(cursor.pos(), BytePos::new(0));
487 }
488
489 #[test]
490 fn test_second_is_none_past_end() {
491 let cursor = Cursor::new("a");
492 assert_eq!(cursor.first(), Some('a'));
493 assert_eq!(cursor.second(), None);
494 }
495
496 #[test]
497 fn test_bump_if_only_consumes_on_match() {
498 let mut cursor = Cursor::new("=>");
499 assert!(!cursor.bump_if('!'));
500 assert_eq!(cursor.pos(), BytePos::new(0));
501 assert!(cursor.bump_if('='));
502 assert_eq!(cursor.pos(), BytePos::new(1));
503 }
504
505 #[test]
506 fn test_eat_while_stops_at_predicate_boundary() {
507 let mut cursor = Cursor::new("123abc");
508 cursor.eat_while(|c| c.is_ascii_digit());
509 assert_eq!(cursor.lexeme(), "123");
510 assert_eq!(cursor.remaining(), "abc");
511 }
512
513 #[test]
514 fn test_lexeme_and_span_track_the_current_run() {
515 let mut cursor = Cursor::with_base("let x", 5);
516 cursor.eat_while(|c| c.is_ascii_alphabetic());
517 assert_eq!(cursor.lexeme(), "let");
518 assert_eq!(cursor.token_span(), Span::new(5, 8));
519 }
520
521 #[test]
522 fn test_emit_resets_the_token_start() {
523 let mut cursor = Cursor::new("ab");
524 cursor.bump();
525 assert_eq!(cursor.emit("a"), Token::new("a", Span::new(0, 1)));
526 // Lexeme and span are now empty at the boundary.
527 assert_eq!(cursor.lexeme(), "");
528 assert_eq!(cursor.token_span(), Span::new(1, 1));
529 cursor.bump();
530 assert_eq!(cursor.emit("b"), Token::new("b", Span::new(1, 2)));
531 }
532
533 #[test]
534 fn test_emit_at_eof_is_an_empty_span() {
535 let mut cursor = Cursor::new("a");
536 cursor.bump();
537 let _ = cursor.emit("a");
538 let eof = cursor.emit("eof");
539 assert_eq!(eof, Token::new("eof", Span::new(1, 1)));
540 assert!(eof.span().is_empty());
541 }
542
543 #[test]
544 fn test_reset_token_drops_the_run_without_emitting() {
545 let mut cursor = Cursor::new(" x");
546 cursor.eat_while(char::is_whitespace);
547 cursor.reset_token();
548 cursor.eat_while(|c| c.is_ascii_alphabetic());
549 // The whitespace is not folded into the identifier's span.
550 assert_eq!(cursor.lexeme(), "x");
551 assert_eq!(cursor.token_span(), Span::new(2, 3));
552 }
553
554 #[test]
555 fn test_reset_token_then_emit_spans_only_the_kept_run() {
556 let mut cursor = Cursor::new("// c\nv");
557 cursor.eat_while(|c| c != '\n'); // a comment to drop
558 cursor.reset_token();
559 cursor.bump(); // the '\n'
560 cursor.reset_token();
561 cursor.bump(); // 'v'
562 assert_eq!(cursor.emit("ident"), Token::new("ident", Span::new(5, 6)));
563 }
564
565 #[test]
566 fn test_positions_saturate_instead_of_wrapping() {
567 // A base one byte below the ceiling: advancing past it saturates at u32::MAX
568 // rather than wrapping around to a tiny offset.
569 let mut cursor = Cursor::with_base("ab", u32::MAX - 1);
570 assert_eq!(cursor.pos(), BytePos::new(u32::MAX - 1));
571 cursor.bump();
572 assert_eq!(cursor.pos(), BytePos::new(u32::MAX));
573 cursor.bump();
574 assert_eq!(cursor.pos(), BytePos::new(u32::MAX)); // saturated, not wrapped
575 }
576
577 #[test]
578 fn test_multibyte_chars_advance_by_byte_length() {
579 // "αβ" is four bytes: each Greek letter is two.
580 let mut cursor = Cursor::new("αβ");
581 assert_eq!(cursor.bump(), Some('α'));
582 assert_eq!(cursor.pos(), BytePos::new(2));
583 assert_eq!(cursor.bump(), Some('β'));
584 assert_eq!(cursor.pos(), BytePos::new(4));
585 assert!(cursor.is_eof());
586 }
587
588 #[test]
589 fn test_lexeme_slices_on_char_boundaries() {
590 let mut cursor = Cursor::new("αβγ");
591 cursor.bump();
592 cursor.bump();
593 assert_eq!(cursor.lexeme(), "αβ");
594 }
595
596 #[test]
597 fn test_for_source_offsets_spans_into_global_space() {
598 use source_lang::SourceMap;
599
600 let mut map = SourceMap::new();
601 map.add("a", "first").expect("fits");
602 let id = map.add("b", "xy").expect("fits");
603 let file = map.source(id).expect("just added");
604
605 let mut cursor = Cursor::for_source(file);
606 assert_eq!(cursor.pos().to_u32(), 5);
607 cursor.eat_while(|_| true);
608 assert_eq!(cursor.token_span(), Span::new(5, 7));
609 }
610
611 #[test]
612 fn test_intern_lexeme_interns_the_current_run() {
613 let mut interner = Interner::new();
614 let mut cursor = Cursor::new("foo bar");
615 cursor.eat_while(|c| c.is_ascii_alphabetic());
616 let foo = cursor.intern_lexeme(&mut interner);
617 assert_eq!(interner.resolve(foo), Some("foo"));
618 }
619
620 #[test]
621 fn test_full_lex_tiles_the_source() {
622 // Lexing the whole input, the emitted spans cover it with no gap or overlap.
623 let src = "ab cd";
624 let mut cursor = Cursor::new(src);
625 let mut spans: Vec<Span> = Vec::new();
626 let mut lexemes = String::new();
627 while !cursor.is_eof() {
628 let c = cursor.first().unwrap();
629 if c.is_whitespace() {
630 cursor.eat_while(char::is_whitespace);
631 } else {
632 cursor.eat_while(|c| !c.is_whitespace());
633 }
634 let tok = cursor.emit(());
635 lexemes.push_str(&src[tok.span().start().to_usize()..tok.span().end().to_usize()]);
636 spans.push(tok.span());
637 }
638 // Spans tile [0, len) exactly.
639 assert_eq!(spans.first().unwrap().start(), BytePos::new(0));
640 assert_eq!(spans.last().unwrap().end(), BytePos::new(src.len() as u32));
641 for pair in spans.windows(2) {
642 assert_eq!(pair[0].end(), pair[1].start());
643 }
644 assert_eq!(lexemes, src);
645 }
646}