1use std::collections::HashMap;
19
20use codehelion_core::frontend::{
21 Diagnostic, DiagnosticKind, SourceSpan, Token, TokenKind, Unit, UnitKind,
22};
23
24use crate::dialect::Dialect;
25
26const TRAILER_KEYWORDS: &[&str] = &[
29 "const", "volatile", "noexcept", "throw", "mutable", "auto", "decltype", "unsigned", "signed",
30 "long", "short", "int", "char", "float", "double", "bool", "void", "typename", "restrict",
31 "_Atomic",
32];
33
34const TRAILER_PUNCT: &[&str] = &["::", "<", ">", ">>", "*", "&", "&&", "->"];
36
37const LAMBDA_PRECEDER_PUNCT: &[&str] = &[
39 "=", "(", ",", "{", ";", ":", "?", "&&", "||", "!", "<", ">", "<<", ">>", "+", "-", "*", "/",
40 "%",
41];
42
43const LAMBDA_PRECEDER_KEYWORDS: &[&str] = &[
45 "return",
46 "co_return",
47 "co_yield",
48 "co_await",
49 "case",
50 "else",
51 "do",
52];
53
54const TRAILER_GROUP_KEYWORDS: &[&str] = &["noexcept", "throw", "decltype"];
57
58const LAMBDA_TRAILER_KEYWORDS: &[&str] = &[
61 "mutable",
62 "noexcept",
63 "constexpr",
64 "consteval",
65 "static",
66 "throw",
67 "decltype",
68 "auto",
69 "const",
70 "unsigned",
71 "signed",
72 "long",
73 "short",
74 "int",
75 "char",
76 "float",
77 "double",
78 "bool",
79 "void",
80 "typename",
81];
82
83const MAX_DECLARATION_LOOKAHEAD: usize = 256;
87
88struct DelimPairs {
91 close_of: HashMap<usize, usize>,
93 open_of: HashMap<usize, usize>,
95}
96
97fn delim_pairs(tokens: &[Token]) -> DelimPairs {
98 let mut close_of = HashMap::new();
99 let mut open_of = HashMap::new();
100 let mut parens = Vec::new();
101 let mut braces = Vec::new();
102 let mut brackets = Vec::new();
103 for (i, token) in tokens.iter().enumerate() {
104 if token.kind != TokenKind::Punctuation {
105 continue;
106 }
107 let (stack, closing) = match token.text.as_str() {
108 "(" | "{" | "[" => {
109 match token.text.as_str() {
110 "(" => parens.push(i),
111 "{" => braces.push(i),
112 _ => brackets.push(i),
113 }
114 continue;
115 }
116 ")" => (&mut parens, i),
117 "}" => (&mut braces, i),
118 "]" => (&mut brackets, i),
119 _ => continue,
120 };
121 if let Some(open) = stack.pop() {
122 close_of.insert(open, closing);
123 open_of.insert(closing, open);
124 }
125 }
126 DelimPairs { close_of, open_of }
127}
128
129#[must_use]
131pub fn detect(tokens: &[Token], dialect: &Dialect) -> (Vec<Unit>, Vec<Diagnostic>) {
132 let pairs = delim_pairs(tokens);
133 let records = record_units(tokens, &pairs, dialect);
134
135 let mut units = records.clone();
136 let mut diagnostics = Vec::new();
137 for (i, token) in tokens.iter().enumerate() {
138 if token.kind == TokenKind::Punctuation && token.text == "{" {
139 if let Some(result) = function_unit(tokens, &pairs, &records, i) {
140 match result {
141 Ok(unit) => units.push(unit),
142 Err(span) => diagnostics.push(Diagnostic {
143 kind: DiagnosticKind::UnmatchedDelimiter,
144 span,
145 }),
146 }
147 }
148 }
149 if dialect.lambdas && token.kind == TokenKind::Punctuation && token.text == "[" {
150 if let Some(unit) = lambda_unit(tokens, &pairs, i) {
151 units.push(unit);
152 }
153 }
154 }
155
156 units.sort_by_key(|u| (u.token_start, u.token_end));
157 (units, diagnostics)
158}
159
160fn record_units(tokens: &[Token], pairs: &DelimPairs, dialect: &Dialect) -> Vec<Unit> {
162 let mut out = Vec::new();
163 for (i, token) in tokens.iter().enumerate() {
164 if token.kind != TokenKind::Keyword
165 || !dialect.record_keywords.contains(&token.text.as_str())
166 {
167 continue;
168 }
169 if let Some(prev) = i.checked_sub(1).map(|p| &tokens[p]) {
170 if prev.kind == TokenKind::Punctuation && matches!(prev.text.as_str(), "<" | ",") {
172 continue;
173 }
174 if prev.kind == TokenKind::Keyword && prev.text == "enum" {
175 continue;
176 }
177 }
178 let Some(open) = record_body_open(tokens, i + 1) else {
179 continue;
180 };
181 let Some(&close) = pairs.close_of.get(&open) else {
182 continue;
183 };
184 let name = tokens[i + 1..open]
185 .iter()
186 .find(|t| t.kind == TokenKind::Identifier)
187 .map(|t| t.text.to_string());
188 out.push(Unit {
189 kind: UnitKind::Record,
190 name,
191 token_start: i,
192 token_end: close + 1,
193 span: span_of(tokens, i, close),
194 });
195 }
196 out
197}
198
199fn record_body_open(tokens: &[Token], from: usize) -> Option<usize> {
205 for (offset, token) in tokens[from..]
206 .iter()
207 .take(MAX_DECLARATION_LOOKAHEAD)
208 .enumerate()
209 {
210 if token.kind == TokenKind::Punctuation {
211 match token.text.as_str() {
212 "{" => return Some(from + offset),
213 ";" | "(" | ")" | "=" => return None,
214 _ => {}
215 }
216 }
217 }
218 None
219}
220
221fn function_unit(
223 tokens: &[Token],
224 pairs: &DelimPairs,
225 records: &[Unit],
226 body_open: usize,
227) -> Option<Result<Unit, SourceSpan>> {
228 let mut j = body_open.checked_sub(1)?;
230 for _ in 0..64 {
231 let token = &tokens[j];
232 match token.kind {
233 TokenKind::Identifier => j = j.checked_sub(1)?,
234 TokenKind::Keyword if TRAILER_KEYWORDS.contains(&token.text.as_str()) => {
235 j = j.checked_sub(1)?;
236 }
237 TokenKind::Punctuation if TRAILER_PUNCT.contains(&token.text.as_str()) => {
238 j = j.checked_sub(1)?;
239 }
240 TokenKind::Punctuation if token.text == ")" => {
241 let &open = pairs.open_of.get(&j)?;
242 if let Some(before) = open.checked_sub(1) {
245 let b = &tokens[before];
246 if b.kind == TokenKind::Keyword
247 && TRAILER_GROUP_KEYWORDS.contains(&b.text.as_str())
248 {
249 j = before.checked_sub(1)?;
250 continue;
251 }
252 }
253 return resolve_signature(tokens, pairs, records, j, body_open);
254 }
255 TokenKind::Punctuation if token.text == "}" => {
258 return resolve_signature(tokens, pairs, records, j, body_open);
259 }
260 _ => return None,
261 }
262 }
263 None
264}
265
266fn resolve_signature(
269 tokens: &[Token],
270 pairs: &DelimPairs,
271 records: &[Unit],
272 close: usize,
273 body_open: usize,
274) -> Option<Result<Unit, SourceSpan>> {
275 let mut close = close;
276 for _ in 0..32 {
277 let &open = pairs.open_of.get(&close)?;
278 let name_i = open.checked_sub(1)?;
279 let name_token = &tokens[name_i];
280
281 if name_token.kind == TokenKind::Identifier {
282 if let Some(sep_i) = name_i.checked_sub(1) {
283 let sep = &tokens[sep_i];
284 if sep.kind == TokenKind::Punctuation && matches!(sep.text.as_str(), ":" | ",") {
285 let prev_i = sep_i.checked_sub(1)?;
289 let prev = &tokens[prev_i];
290 if prev.kind == TokenKind::Punctuation
291 && matches!(prev.text.as_str(), ")" | "}")
292 {
293 close = prev_i;
294 continue;
295 }
296 return None;
297 }
298 }
299 if tokens[close].text != ")" {
301 return None;
302 }
303 let inside_record = records
308 .iter()
309 .any(|record| record.token_start < name_i && name_i < record.token_end);
310 if !inside_record && !has_declaration_prefix(tokens, name_i) {
311 return None;
312 }
313 let tilde = name_i
314 .checked_sub(1)
315 .is_some_and(|p| tokens[p].kind == TokenKind::Punctuation && tokens[p].text == "~");
316 let unit_start = if tilde { name_i - 1 } else { name_i };
317 return Some(make_function(
318 tokens,
319 pairs,
320 records,
321 unit_start,
322 name_i,
323 name_token.text.to_string(),
324 body_open,
325 ));
326 }
327
328 if tokens[close].text == ")" {
331 for back in 1..=3 {
332 let Some(k) = open.checked_sub(back) else {
333 break;
334 };
335 if tokens[k].kind == TokenKind::Keyword && tokens[k].text == "operator" {
336 return Some(make_function(
337 tokens,
338 pairs,
339 records,
340 k,
341 k,
342 "operator".to_string(),
343 body_open,
344 ));
345 }
346 }
347 }
348 return None;
349 }
350 None
351}
352
353fn has_declaration_prefix(tokens: &[Token], name_i: usize) -> bool {
359 name_i.checked_sub(1).is_some_and(|previous| {
360 let token = &tokens[previous];
361 matches!(token.kind, TokenKind::Identifier | TokenKind::Keyword)
362 || (token.kind == TokenKind::Punctuation
363 && matches!(token.text.as_str(), "*" | "&" | "&&" | "::" | "~"))
364 })
365}
366
367fn make_function(
368 tokens: &[Token],
369 pairs: &DelimPairs,
370 records: &[Unit],
371 unit_start: usize,
372 name_i: usize,
373 name: String,
374 body_open: usize,
375) -> Result<Unit, SourceSpan> {
376 let end = pairs.close_of.get(&body_open).copied().ok_or_else(|| {
377 span_of(tokens, body_open, body_open)
381 })?;
382 let inside_record = records
383 .iter()
384 .any(|r| r.token_start < name_i && name_i < r.token_end);
385 let kind = if inside_record {
386 UnitKind::Method
387 } else {
388 UnitKind::Function
389 };
390 Ok(Unit {
391 kind,
392 name: Some(name),
393 token_start: unit_start,
394 token_end: end + 1,
395 span: span_of(tokens, unit_start, end),
396 })
397}
398
399fn lambda_unit(tokens: &[Token], pairs: &DelimPairs, i: usize) -> Option<Unit> {
401 if let Some(prev) = i.checked_sub(1).map(|p| &tokens[p]) {
402 let allowed = match prev.kind {
403 TokenKind::Punctuation => LAMBDA_PRECEDER_PUNCT.contains(&prev.text.as_str()),
404 TokenKind::Keyword => LAMBDA_PRECEDER_KEYWORDS.contains(&prev.text.as_str()),
405 _ => false,
406 };
407 if !allowed {
408 return None;
409 }
410 }
411 let &capture_close = pairs.close_of.get(&i)?;
412
413 let mut k = capture_close + 1;
415 if tokens.get(k).is_some_and(|t| t.text == "(") {
416 k = pairs.close_of.get(&k)? + 1;
417 }
418
419 for _ in 0..32 {
421 let token = tokens.get(k)?;
422 match token.kind {
423 TokenKind::Punctuation if token.text == "{" => {
424 let &close = pairs.close_of.get(&k)?;
425 return Some(Unit {
426 kind: UnitKind::Closure,
427 name: None,
428 token_start: i,
429 token_end: close + 1,
430 span: span_of(tokens, i, close),
431 });
432 }
433 TokenKind::Punctuation if TRAILER_PUNCT.contains(&token.text.as_str()) => k += 1,
434 TokenKind::Punctuation if token.text == "(" => {
435 k = pairs.close_of.get(&k)? + 1;
437 }
438 TokenKind::Identifier => k += 1,
439 TokenKind::Keyword if LAMBDA_TRAILER_KEYWORDS.contains(&token.text.as_str()) => k += 1,
440 _ => return None,
441 }
442 }
443 None
444}
445
446fn span_of(tokens: &[Token], start: usize, end: usize) -> SourceSpan {
448 let first = tokens[start].span;
449 let last = tokens[end].span;
450 SourceSpan {
451 start_byte: first.start_byte,
452 end_byte: last.end_byte,
453 start_line: first.start_line,
454 start_column: first.start_column,
455 }
456}
457
458#[cfg(test)]
459#[allow(clippy::expect_used, clippy::unwrap_used)]
460mod tests {
461 use super::*;
462 use crate::dialect;
463 use crate::lexer::lex;
464
465 fn units_of(source: &str) -> Vec<Unit> {
466 detect(&lex(source, &dialect::C).0, &dialect::C).0
467 }
468
469 #[test]
470 fn detects_a_free_function() {
471 let units = units_of("int add(int a, int b) { return a + b; }");
472 assert_eq!(units.len(), 1);
473 assert_eq!(units[0].kind, UnitKind::Function);
474 assert_eq!(units[0].name.as_deref(), Some("add"));
475 }
476
477 #[test]
478 fn prototypes_are_not_units() {
479 assert!(units_of("int add(int a, int b);").is_empty());
480 assert!(units_of("extern void log_msg(const char *fmt, ...);").is_empty());
481 }
482
483 #[test]
484 fn control_flow_braces_are_not_functions() {
485 let src = "void f(int n) { if (n) { g(); } while (n--) { h(); } \
486 for (;;) { break; } switch (n) { default: break; } do { i(); } while (0); }";
487 let units = units_of(src);
488 assert_eq!(units.len(), 1, "only `f` itself: {units:#?}");
489 assert_eq!(units[0].name.as_deref(), Some("f"));
490 }
491
492 #[test]
493 fn pointer_returning_and_static_functions_are_detected() {
494 let units = units_of("static const char *dup(const char *s) { return s; }");
495 assert_eq!(units.len(), 1);
496 assert_eq!(units[0].kind, UnitKind::Function);
497 assert_eq!(units[0].name.as_deref(), Some("dup"));
498 }
499
500 #[test]
501 fn struct_definitions_are_records_but_declarators_are_not() {
502 let units = units_of("struct point { int x; int y; };");
503 assert_eq!(units.len(), 1);
504 assert_eq!(units[0].kind, UnitKind::Record);
505 assert_eq!(units[0].name.as_deref(), Some("point"));
506
507 let units = units_of("struct point *make(void) { return 0; }");
509 assert_eq!(units.len(), 1, "{units:#?}");
510 assert_eq!(units[0].kind, UnitKind::Function);
511 assert_eq!(units[0].name.as_deref(), Some("make"));
512 }
513
514 #[test]
515 fn anonymous_typedef_struct_is_a_record_without_a_name() {
516 let units = units_of("typedef struct { int a; } pair;");
517 assert_eq!(units.len(), 1);
518 assert_eq!(units[0].kind, UnitKind::Record);
519 assert_eq!(units[0].name, None);
522 }
523
524 #[test]
525 fn function_like_macro_bodies_do_not_produce_units() {
526 let units = units_of("#define ADD(a, b) ((a) + (b))\n");
528 assert!(units.is_empty());
529 }
530
531 #[test]
532 fn block_bodied_macro_invocations_are_not_function_units() {
533 for invocation in [
534 "TEST_F(QueueTest, Pushes) { ASSERT_TRUE(1); }",
535 "list_for_each(node, head) { visit(node); }",
536 "TAILQ_FOREACH(entry, queue, links) { consume(entry); }",
537 ] {
538 assert!(units_of(invocation).is_empty(), "{invocation}");
539 }
540 }
541
542 #[test]
543 fn initializer_braces_are_not_functions() {
544 assert!(units_of("int a[] = {1, 2, 3};").is_empty());
545 assert!(
546 units_of("struct p q = {1, 2};")
547 .iter()
548 .all(|u| u.kind != UnitKind::Function)
549 );
550 }
551
552 #[test]
553 fn a_units_token_range_covers_its_body() {
554 let src = "int f(void) { return 1; }";
555 let tokens = lex(src, &dialect::C).0;
556 let (units, diagnostics) = detect(&tokens, &dialect::C);
557 assert!(diagnostics.is_empty());
558 let f = &units[0];
559 assert_eq!(tokens[f.token_end - 1].text, "}");
560 assert_eq!(tokens[f.token_start].text, "f");
561 }
562
563 #[test]
564 fn record_declaration_lookahead_is_bounded() {
565 let source = format!(
566 "struct {} {{ int value; }};",
567 "field ".repeat(MAX_DECLARATION_LOOKAHEAD)
568 );
569 let tokens = lex(&source, &dialect::C).0;
570 assert_eq!(record_body_open(&tokens, 1), None);
571 }
572
573 #[test]
574 fn an_unclosed_function_body_is_not_stretched_to_end_of_file() {
575 let tokens = lex("int tail(void) { int value = 1;", &dialect::C).0;
576 let (units, diagnostics) = detect(&tokens, &dialect::C);
577
578 assert!(units.is_empty());
579 assert_eq!(diagnostics.len(), 1);
580 assert_eq!(diagnostics[0].kind, DiagnosticKind::UnmatchedDelimiter);
581 }
582}