Skip to main content

codehelion_core/
frontend.rs

1//! The Fast-frontend interface.
2//!
3//! A frontend turns a source file into a flat token stream with comments and
4//! whitespace removed, plus the coarse unit boundaries (functions, methods,
5//! `impl` blocks, closures) used later as clone-report anchors. Lexing is
6//! error-tolerant: a malformed span becomes a [`Diagnostic`] and lexing
7//! continues, so one broken construct never discards the rest of a file.
8//!
9//! The frontend deliberately stops at lexing. Macros and templates are not
10//! expanded; their invocations pass through as ordinary tokens. Normalization
11//! (identifier renaming, literal folding) is applied downstream at fragment
12//! scope, so the stream here carries each token's raw lexeme unchanged.
13//!
14//! Source positions are recorded for reporting only. They are never used as
15//! stable identifiers: fingerprints are built from token kinds and normalized
16//! text, never from a line number or token offset.
17
18use std::borrow::Borrow;
19use std::collections::HashSet;
20use std::fmt;
21use std::ops::Deref;
22use std::sync::Arc;
23
24use crate::discovery::Language;
25
26/// A shared, immutable lexeme.
27///
28/// Token text is stored behind a shared pointer so that every occurrence of
29/// the same lexeme in a file shares one allocation instead of owning a copy;
30/// with millions of tokens in scope this is the difference between hundreds
31/// of megabytes and a few. Equality, ordering and hashing follow the text
32/// content, and the type dereferences to [`str`], so call sites treat it
33/// like a borrowed string.
34#[derive(Debug, Clone, Eq)]
35pub struct Lexeme(Arc<str>);
36
37impl Lexeme {
38    /// The lexeme text.
39    #[must_use]
40    pub fn as_str(&self) -> &str {
41        &self.0
42    }
43}
44
45impl PartialEq for Lexeme {
46    fn eq(&self, other: &Self) -> bool {
47        // Interned lexemes of one file share their allocation, so pointer
48        // identity settles most comparisons without touching the bytes.
49        Arc::ptr_eq(&self.0, &other.0) || self.0 == other.0
50    }
51}
52
53impl std::hash::Hash for Lexeme {
54    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
55        // Must agree with `str::hash` for `Borrow<str>` lookups.
56        self.0.hash(state);
57    }
58}
59
60impl Deref for Lexeme {
61    type Target = str;
62
63    fn deref(&self) -> &str {
64        &self.0
65    }
66}
67
68impl AsRef<str> for Lexeme {
69    fn as_ref(&self) -> &str {
70        &self.0
71    }
72}
73
74impl Borrow<str> for Lexeme {
75    fn borrow(&self) -> &str {
76        &self.0
77    }
78}
79
80impl From<&str> for Lexeme {
81    fn from(text: &str) -> Self {
82        Self(Arc::from(text))
83    }
84}
85
86impl PartialEq<str> for Lexeme {
87    fn eq(&self, other: &str) -> bool {
88        &*self.0 == other
89    }
90}
91
92impl PartialEq<&str> for Lexeme {
93    fn eq(&self, other: &&str) -> bool {
94        &*self.0 == *other
95    }
96}
97
98impl fmt::Display for Lexeme {
99    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100        f.write_str(&self.0)
101    }
102}
103
104/// Deduplicating store of [`Lexeme`]s, typically one per lexed file.
105///
106/// Interning the same text twice returns two handles to one allocation. The
107/// interner is an implementation detail of memory layout: it never affects
108/// token equality or fingerprints, which follow text content only.
109#[derive(Debug, Default)]
110pub struct LexemeInterner {
111    known: HashSet<Lexeme>,
112}
113
114impl LexemeInterner {
115    /// Create an empty interner.
116    #[must_use]
117    pub fn new() -> Self {
118        Self::default()
119    }
120
121    /// Return the shared lexeme for `text`, allocating it once.
122    pub fn intern(&mut self, text: &str) -> Lexeme {
123        if let Some(found) = self.known.get(text) {
124            return found.clone();
125        }
126        let lexeme = Lexeme::from(text);
127        self.known.insert(lexeme.clone());
128        lexeme
129    }
130}
131
132/// Category of a literal token, used by literal-normalization strategies.
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub enum LiteralKind {
135    /// Integer literal, e.g. `42`, `0xff`, `1_000`.
136    Integer,
137    /// Floating-point literal, e.g. `1.5`, `2e10`.
138    Float,
139    /// String literal, including raw and byte strings.
140    String,
141    /// Character or byte-character literal.
142    Char,
143    /// Boolean literal (`true` / `false`).
144    Bool,
145}
146
147/// The lexical category of a token.
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub enum TokenKind {
150    /// An identifier (including raw identifiers).
151    Identifier,
152    /// A language keyword.
153    Keyword,
154    /// A literal of the given category.
155    Literal(LiteralKind),
156    /// A lifetime or label, e.g. `'a`.
157    Lifetime,
158    /// An operator or delimiter.
159    Punctuation,
160    /// Input that could not be lexed into any of the above.
161    Unknown,
162}
163
164impl TokenKind {
165    /// A stable one-byte tag for this kind, for use as fingerprint input.
166    ///
167    /// The literal sub-category does not affect the tag: whether two literals
168    /// are considered equal is a normalization decision, made downstream.
169    #[must_use]
170    pub const fn tag(self) -> u8 {
171        match self {
172            Self::Identifier => 1,
173            Self::Keyword => 2,
174            Self::Literal(_) => 3,
175            Self::Punctuation => 4,
176            Self::Lifetime => 5,
177            Self::Unknown => 6,
178        }
179    }
180}
181
182/// A source position span, recorded for reporting only.
183///
184/// `start_byte`/`end_byte` are byte offsets into the source; `start_line` and
185/// `start_column` are 1-based and counted in characters. None of these fields
186/// may be used to derive a stable identifier.
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub struct SourceSpan {
189    /// Byte offset of the span start.
190    pub start_byte: usize,
191    /// Byte offset one past the span end.
192    pub end_byte: usize,
193    /// 1-based line of the span start.
194    pub start_line: u32,
195    /// 1-based column (in characters) of the span start.
196    pub start_column: u32,
197}
198
199/// One lexical token.
200#[derive(Debug, Clone, PartialEq, Eq)]
201pub struct Token {
202    /// Lexical category.
203    pub kind: TokenKind,
204    /// The raw lexeme exactly as it appeared in the source.
205    pub text: Lexeme,
206    /// Source position, for reporting only.
207    pub span: SourceSpan,
208}
209
210/// The kind of a recoverable Fast-frontend problem.
211#[derive(Debug, Clone, Copy, PartialEq, Eq)]
212pub enum DiagnosticKind {
213    /// A string literal was not closed before end of file.
214    UnterminatedString,
215    /// A character or byte literal was not closed before end of file.
216    UnterminatedChar,
217    /// A block comment was not closed before end of file.
218    UnterminatedBlockComment,
219    /// A byte that does not begin any valid token.
220    UnexpectedCharacter,
221    /// An opening delimiter needed by a unit boundary had no matching closer.
222    UnmatchedDelimiter,
223}
224
225/// A recoverable Fast-frontend problem. Analysis continues past it.
226#[derive(Debug, Clone, PartialEq, Eq)]
227pub struct Diagnostic {
228    /// What went wrong.
229    pub kind: DiagnosticKind,
230    /// Where it happened.
231    pub span: SourceSpan,
232}
233
234/// The kind of a coarse code unit used as a clone-report anchor.
235#[derive(Debug, Clone, Copy, PartialEq, Eq)]
236pub enum UnitKind {
237    /// A free function.
238    Function,
239    /// A method (a function inside an `impl` block or a record body).
240    Method,
241    /// An `impl` block.
242    Impl,
243    /// A record body: a `class`, `struct` or `union` definition.
244    Record,
245    /// A closure or lambda with a block body.
246    Closure,
247}
248
249impl UnitKind {
250    /// Stable lowercase identifier used in reports.
251    #[must_use]
252    pub const fn name(self) -> &'static str {
253        match self {
254            Self::Function => "function",
255            Self::Method => "method",
256            Self::Impl => "impl",
257            Self::Record => "record",
258            Self::Closure => "closure",
259        }
260    }
261}
262
263/// A coarse code unit: a token range plus its source span.
264#[derive(Debug, Clone, PartialEq, Eq)]
265pub struct Unit {
266    /// The unit's kind.
267    pub kind: UnitKind,
268    /// The unit's name, when the frontend can recover one.
269    pub name: Option<String>,
270    /// Index of the unit's first token in the stream.
271    pub token_start: usize,
272    /// Index one past the unit's last token in the stream.
273    pub token_end: usize,
274    /// Source span covering the unit, for reporting.
275    pub span: SourceSpan,
276}
277
278/// The result of lexing one source file.
279#[derive(Debug, Clone)]
280pub struct LexedFile {
281    /// Language the file was lexed as.
282    pub language: Language,
283    /// Version tag of the frontend that produced this result; a fingerprint
284    /// input, so a change to lexing that alters output must change it.
285    pub frontend_version: &'static str,
286    /// Tokens in source order, comments and whitespace removed.
287    pub tokens: Vec<Token>,
288    /// Coarse unit boundaries, in source order.
289    pub units: Vec<Unit>,
290    /// Recoverable problems encountered while lexing.
291    pub diagnostics: Vec<Diagnostic>,
292}
293
294/// A Fast-mode lexer for one language.
295pub trait Frontend {
296    /// The language this frontend lexes.
297    fn language(&self) -> Language;
298
299    /// The frontend's version tag, used as a fingerprint input.
300    fn frontend_version(&self) -> &'static str;
301
302    /// Lex `source` into a token stream with unit boundaries and diagnostics.
303    fn lex(&self, source: &str) -> LexedFile;
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309
310    #[test]
311    fn kind_tags_are_distinct_and_stable() {
312        let tags = [
313            TokenKind::Identifier.tag(),
314            TokenKind::Keyword.tag(),
315            TokenKind::Literal(LiteralKind::Integer).tag(),
316            TokenKind::Punctuation.tag(),
317            TokenKind::Lifetime.tag(),
318            TokenKind::Unknown.tag(),
319        ];
320        let mut sorted = tags.to_vec();
321        sorted.sort_unstable();
322        sorted.dedup();
323        assert_eq!(sorted.len(), tags.len(), "tags must be distinct");
324        // The literal sub-category does not change the tag.
325        assert_eq!(
326            TokenKind::Literal(LiteralKind::Integer).tag(),
327            TokenKind::Literal(LiteralKind::String).tag()
328        );
329    }
330
331    #[test]
332    fn interning_shares_one_allocation_per_text() {
333        let mut interner = LexemeInterner::new();
334        let a = interner.intern("alpha");
335        let b = interner.intern("alpha");
336        let c = interner.intern("beta");
337        assert!(Arc::ptr_eq(&a.0, &b.0), "same text must share storage");
338        assert!(!Arc::ptr_eq(&a.0, &c.0));
339        assert_eq!(a, b);
340        assert_ne!(a, c);
341    }
342
343    #[test]
344    fn lexeme_equality_and_hash_follow_content_across_interners() {
345        let a = LexemeInterner::new().intern("shared");
346        let b = LexemeInterner::new().intern("shared");
347        assert!(
348            !Arc::ptr_eq(&a.0, &b.0),
349            "distinct interners allocate separately"
350        );
351        assert_eq!(a, b, "equality is by content, not by pointer");
352        let set: HashSet<Lexeme> = [a].into();
353        assert!(set.contains("shared"), "str lookups must hash consistently");
354    }
355
356    #[test]
357    fn lexeme_compares_against_plain_strings() {
358        let lexeme = Lexeme::from("fn");
359        assert_eq!(lexeme, "fn");
360        assert_eq!(lexeme.as_str(), "fn");
361        assert_eq!(lexeme.to_string(), "fn");
362        assert_eq!(lexeme.as_bytes(), b"fn");
363    }
364
365    #[test]
366    fn unit_kind_names_are_stable() {
367        assert_eq!(UnitKind::Function.name(), "function");
368        assert_eq!(UnitKind::Method.name(), "method");
369        assert_eq!(UnitKind::Impl.name(), "impl");
370        assert_eq!(UnitKind::Closure.name(), "closure");
371    }
372}