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}