lexer_lang/lib.rs
1//! # lexer_lang
2//!
3//! The scanner that turns source text into a token stream.
4//!
5//! lexer-lang provides the [`Cursor`] — a zero-copy scanner over a `&str` — the
6//! primitive every lexer is built on, hand-written or generated. A lexer drives the
7//! cursor with peek/advance steps and turns each scanned run into a
8//! [`Token<K>`](Token), where `K` is the language's own token kind. The cursor owns
9//! scanning and nothing else: it does not decide a language's keywords (that is `K`),
10//! collect diagnostics (that is the lexer author's, through `diag-lang`), or store
11//! sources (that is `source-lang`).
12//!
13//! It is the first MAIN-tier crate: the seam where a front-end stops handling raw
14//! bytes and starts handling tokens.
15//!
16//! ## The model
17//!
18//! A lexer is a loop. Look at the next character, decide what kind of token starts
19//! there, consume its run, and [`emit`](Cursor::emit) a token. The cursor supplies
20//! exactly the primitives that loop needs:
21//!
22//! - look ahead — [`first`](Cursor::first), [`second`](Cursor::second),
23//! [`is_eof`](Cursor::is_eof);
24//! - consume — [`bump`](Cursor::bump), [`bump_if`](Cursor::bump_if),
25//! [`eat_while`](Cursor::eat_while);
26//! - read the run — [`lexeme`](Cursor::lexeme), [`token_span`](Cursor::token_span),
27//! [`intern_lexeme`](Cursor::intern_lexeme);
28//! - finish a token — [`emit`](Cursor::emit), which builds the `Token<K>` and starts
29//! the next, or [`reset_token`](Cursor::reset_token), which drops the run (for
30//! trivia the lexer discards rather than emits).
31//!
32//! Everything is zero-copy: the cursor borrows the source, reports positions as
33//! [`BytePos`] and [`Span`], and hands lexemes back as borrowed `&str`. Nothing here
34//! allocates.
35//!
36//! ## Positions
37//!
38//! The spans a cursor emits live in a global position space offset by a base.
39//! [`Cursor::for_source`] sets that base from a [`SourceFile`], so the spans resolve
40//! directly against the [`SourceMap`](source_lang::SourceMap) a diagnostic renders
41//! through — token spans and error labels share one coordinate space.
42//!
43//! ## Quickstart
44//!
45//! ```
46//! use intern_lang::Interner;
47//! use token_lang::{Symbol, TokenKind};
48//! use lexer_lang::Cursor;
49//!
50//! #[derive(Clone, Copy, Debug, PartialEq, Eq)]
51//! enum Kind {
52//! Ident(Symbol),
53//! Number,
54//! Whitespace,
55//! Eof,
56//! }
57//! impl TokenKind for Kind {
58//! fn is_trivia(&self) -> bool { matches!(self, Kind::Whitespace) }
59//! fn is_eof(&self) -> bool { matches!(self, Kind::Eof) }
60//! fn symbol(&self) -> Option<Symbol> {
61//! match self { Kind::Ident(s) => Some(*s), _ => None }
62//! }
63//! }
64//!
65//! let mut interner = Interner::new();
66//! let mut cursor = Cursor::new("x42 7");
67//! let mut kinds = Vec::new();
68//! while let Some(c) = cursor.first() {
69//! let kind = if c.is_whitespace() {
70//! cursor.eat_while(char::is_whitespace);
71//! Kind::Whitespace
72//! } else if c.is_ascii_digit() {
73//! cursor.eat_while(|c| c.is_ascii_digit());
74//! Kind::Number
75//! } else {
76//! cursor.eat_while(|c| c.is_ascii_alphanumeric());
77//! Kind::Ident(cursor.intern_lexeme(&mut interner))
78//! };
79//! kinds.push(cursor.emit(kind).into_kind());
80//! }
81//! assert!(matches!(kinds[0], Kind::Ident(_)));
82//! assert_eq!(kinds[1], Kind::Whitespace);
83//! assert_eq!(kinds[2], Kind::Number);
84//! ```
85//!
86//! ## `no_std`
87//!
88//! The crate is `no_std`-compatible and allocation-free: the cursor borrows its
89//! source and needs neither the standard library nor `alloc`. The default `std`
90//! feature only forwards to the standard-library builds of the family crates.
91//! Disable default features to build for a `no_std` target.
92//!
93//! ## Stability
94//!
95//! As of `1.0.0` the public surface is **stable** and follows Semantic Versioning:
96//! no breaking changes before `2.0`, additions arrive in minor releases, and the
97//! MSRV (Rust 1.85) only rises in a minor. The frozen surface is catalogued in
98//! [`docs/API.md`](https://github.com/jamesgober/lexer-lang/blob/main/docs/API.md).
99
100#![cfg_attr(not(feature = "std"), no_std)]
101#![cfg_attr(docsrs, feature(doc_cfg))]
102#![forbid(unsafe_code)]
103#![deny(missing_docs)]
104#![deny(unused_must_use)]
105#![deny(unused_results)]
106#![deny(clippy::unwrap_used)]
107#![deny(clippy::expect_used)]
108#![deny(clippy::todo)]
109#![deny(clippy::unimplemented)]
110#![deny(clippy::print_stdout)]
111#![deny(clippy::print_stderr)]
112#![deny(clippy::dbg_macro)]
113#![deny(clippy::unreachable)]
114
115mod cursor;
116
117pub use cursor::Cursor;
118
119// Re-exported so a lexer can name the types the cursor's API speaks without also
120// depending on the family crates directly.
121pub use intern_lang::{Interner, Symbol};
122pub use source_lang::SourceFile;
123pub use span_lang::{BytePos, Span};
124pub use token_lang::{Token, TokenKind};