Skip to main content

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}