1use codehelion_core::discovery::Language;
42use codehelion_core::frontend::{
43 Lexeme, LexemeInterner, LiteralKind, SourceSpan, Token, TokenKind,
44};
45use codehelion_core::ir::{
46 ByteRange, IR_SCHEMA_VERSION, IrNode, MAX_IR_DEPTH, Shape, StructuralFrontend, SyntaxIrFile,
47};
48use tree_sitter::{Node, Parser};
49
50pub const STRUCTURAL_FRONTEND_VERSION: &str = "c-ir-v1";
54
55const ATOMIC_TOKEN_KINDS: &[&str] = &[
60 "string_literal",
61 "char_literal",
62 "system_lib_string",
63 "raw_string_literal",
64];
65
66const COMMENT_KIND: &str = "comment";
68
69#[derive(Debug, Clone)]
71pub enum Mapping {
72 Emit(Shape),
74 Native(&'static str),
76 ExprStmt,
78 Error,
80 Transparent,
82}
83
84pub trait IrMapping {
92 fn classify(&self, node: &Node<'_>) -> Mapping;
98
99 fn node_name<'s>(&self, node: &Node<'_>, source: &'s str) -> Option<&'s str> {
101 c_family_node_name(node, source)
102 }
103
104 fn token_kind(&self, kind: &str, is_named: bool, text: &str) -> TokenKind {
106 classify_token(kind, is_named, text)
107 }
108}
109
110#[must_use]
115pub fn classify_c(node: &Node<'_>) -> Mapping {
116 match node.kind() {
117 "function_definition" => Mapping::Emit(Shape::Function),
118 "compound_statement" => Mapping::Emit(Shape::Block),
119 "for_statement" | "while_statement" | "do_statement" => Mapping::Emit(Shape::Loop),
120 "if_statement" => Mapping::Emit(Shape::Branch),
124 "switch_statement" => Mapping::Emit(Shape::Match),
125 "case_statement" => Mapping::Emit(Shape::MatchArm),
127 "call_expression" => Mapping::Emit(Shape::Call),
128 "assignment_expression" => Mapping::Emit(Shape::Assign),
130 "declaration" => Mapping::Emit(Shape::VarDecl),
131 "return_statement" => Mapping::Emit(Shape::Return),
132 "break_statement" => Mapping::Emit(Shape::Break),
133 "continue_statement" => Mapping::Emit(Shape::Continue),
134 "expression_statement" => Mapping::ExprStmt,
135 "preproc_def" | "preproc_function_def" => Mapping::Emit(Shape::MacroDef),
136 "goto_statement" => Mapping::Native("goto_statement"),
139 "preproc_if" | "preproc_ifdef" | "preproc_else" | "preproc_elif" | "preproc_elifdef" => {
142 Mapping::Native(node.kind())
143 }
144 "struct_specifier" | "union_specifier" | "enum_specifier" => record_mapping(node),
145 "ERROR" => Mapping::Error,
146 _ => Mapping::Transparent,
147 }
148}
149
150#[must_use]
154pub fn record_mapping(node: &Node<'_>) -> Mapping {
155 if node.child_by_field_name("body").is_some() {
156 Mapping::Emit(Shape::Record)
157 } else {
158 Mapping::Transparent
159 }
160}
161
162#[must_use]
170pub fn classify_token(kind: &str, is_named: bool, text: &str) -> TokenKind {
171 match kind {
172 "identifier"
173 | "field_identifier"
174 | "type_identifier"
175 | "statement_identifier"
176 | "namespace_identifier" => TokenKind::Identifier,
177 "primitive_type" | "sized_type_specifier" | "auto" | "this" => TokenKind::Keyword,
181 "null" => {
184 if text == "nullptr" {
185 TokenKind::Keyword
186 } else {
187 TokenKind::Identifier
188 }
189 }
190 "number_literal" => TokenKind::Literal(number_literal_kind(text)),
191 "string_literal" | "system_lib_string" | "raw_string_literal" => {
192 TokenKind::Literal(LiteralKind::String)
193 }
194 "char_literal" => TokenKind::Literal(LiteralKind::Char),
195 "true" | "false" => TokenKind::Literal(LiteralKind::Bool),
196 _ if !is_named => {
197 if !kind.is_empty() && kind.chars().all(|c| c.is_ascii_alphabetic() || c == '_') {
198 TokenKind::Keyword
199 } else {
200 TokenKind::Punctuation
201 }
202 }
203 _ => TokenKind::Unknown,
204 }
205}
206
207fn number_literal_kind(text: &str) -> LiteralKind {
211 let hex = text.starts_with("0x") || text.starts_with("0X");
212 let float = text.contains('.')
213 || if hex {
214 text.contains(['p', 'P'])
215 } else {
216 text.contains(['e', 'E']) || text.ends_with(['f', 'F'])
217 };
218 if float {
219 LiteralKind::Float
220 } else {
221 LiteralKind::Integer
222 }
223}
224
225#[must_use]
229pub fn c_family_node_name<'s>(node: &Node<'_>, source: &'s str) -> Option<&'s str> {
230 match node.kind() {
231 "function_definition" => {
232 declarator_identifier(node.child_by_field_name("declarator")?, source)
233 }
234 "struct_specifier"
235 | "union_specifier"
236 | "enum_specifier"
237 | "class_specifier"
238 | "preproc_def"
239 | "preproc_function_def" => node_text(&node.child_by_field_name("name")?, source),
240 _ => None,
241 }
242}
243
244fn declarator_identifier<'s>(declarator: Node<'_>, source: &'s str) -> Option<&'s str> {
249 let mut current = declarator;
250 loop {
251 match current.kind() {
252 "identifier" | "field_identifier" | "type_identifier" | "operator_name"
253 | "destructor_name" => return node_text(¤t, source),
254 "qualified_identifier" => current = current.child_by_field_name("name")?,
255 "pointer_declarator"
256 | "function_declarator"
257 | "parenthesized_declarator"
258 | "reference_declarator" => {
259 current = current
260 .child_by_field_name("declarator")
261 .or_else(|| current.named_child(0))?;
262 }
263 _ => return None,
264 }
265 }
266}
267
268fn node_text<'s>(node: &Node<'_>, source: &'s str) -> Option<&'s str> {
270 source.get(node.start_byte()..node.end_byte())
271}
272
273fn node_range(node: &Node<'_>) -> ByteRange {
275 ByteRange {
276 start: node.start_byte(),
277 end: node.end_byte(),
278 }
279}
280
281#[must_use]
290pub fn parse_to_ir(
291 source: &str,
292 grammar: &tree_sitter::Language,
293 mapping: &dyn IrMapping,
294 language: Language,
295 frontend_version: &'static str,
296) -> SyntaxIrFile {
297 let mut parser = Parser::new();
298 let tree = if parser.set_language(grammar).is_ok() {
299 parser.parse(source, None)
300 } else {
301 None
302 };
303 let Some(tree) = tree else {
304 return SyntaxIrFile {
305 language,
306 frontend_version,
307 ir_schema_version: IR_SCHEMA_VERSION,
308 tokens: Vec::new(),
309 roots: Vec::new(),
310 diagnostics: Vec::new(),
311 error_ranges: vec![ByteRange {
312 start: 0,
313 end: source.len(),
314 }],
315 depth_truncated: false,
316 test_module: false,
317 };
318 };
319
320 let root = tree.root_node();
321 let mut builder = IrBuilder::new(source, mapping);
322 builder.collect_tokens(root);
323
324 let mut roots = Vec::new();
325 builder.visit(root, &mut roots, 0);
328
329 builder
330 .error_ranges
331 .sort_unstable_by_key(|range| (range.start, range.end));
332 builder.error_ranges.dedup();
333
334 SyntaxIrFile {
335 language,
336 frontend_version,
337 ir_schema_version: IR_SCHEMA_VERSION,
338 tokens: builder.tokens,
339 roots,
340 diagnostics: Vec::new(),
343 error_ranges: builder.error_ranges,
344 depth_truncated: builder.depth_truncated,
345 test_module: false,
346 }
347}
348
349struct IrBuilder<'s, 'm> {
351 source: &'s str,
352 mapping: &'m dyn IrMapping,
353 interner: LexemeInterner,
354 tokens: Vec<Token>,
355 token_starts: Vec<usize>,
358 line_starts: Vec<usize>,
360 error_ranges: Vec<ByteRange>,
361 depth_truncated: bool,
362}
363
364impl<'s, 'm> IrBuilder<'s, 'm> {
365 fn new(source: &'s str, mapping: &'m dyn IrMapping) -> Self {
366 let mut line_starts = vec![0];
367 for (index, byte) in source.bytes().enumerate() {
368 if byte == b'\n' {
369 line_starts.push(index + 1);
370 }
371 }
372 Self {
373 source,
374 mapping,
375 interner: LexemeInterner::new(),
376 tokens: Vec::new(),
377 token_starts: Vec::new(),
378 line_starts,
379 error_ranges: Vec::new(),
380 depth_truncated: false,
381 }
382 }
383
384 fn collect_tokens(&mut self, root: Node<'_>) {
388 let mut cursor = root.walk();
389 loop {
390 let node = cursor.node();
391 let kind = node.kind();
392 let descend = kind != COMMENT_KIND
393 && !ATOMIC_TOKEN_KINDS.contains(&kind)
394 && node.child_count() > 0;
395 if descend && cursor.goto_first_child() {
396 continue;
397 }
398 if !descend && kind != COMMENT_KIND {
399 if node.is_missing() {
400 self.error_ranges.push(node_range(&node));
401 } else if node.end_byte() > node.start_byte() {
402 self.emit_token(&node);
403 }
404 }
405 loop {
406 if cursor.goto_next_sibling() {
407 break;
408 }
409 if !cursor.goto_parent() {
410 return;
411 }
412 }
413 }
414 }
415
416 fn emit_token(&mut self, node: &Node<'_>) {
417 let start_byte = node.start_byte();
418 let end_byte = node.end_byte();
419 let text = node_text(node, self.source).unwrap_or("");
420 let kind = self.mapping.token_kind(node.kind(), node.is_named(), text);
421 let (start_line, start_column) = self.line_column(start_byte);
422 let text = self.interner.intern(text);
423 self.token_starts.push(start_byte);
424 self.tokens.push(Token {
425 kind,
426 text,
427 span: SourceSpan {
428 start_byte,
429 end_byte,
430 start_line,
431 start_column,
432 },
433 });
434 }
435
436 fn line_column(&self, byte: usize) -> (u32, u32) {
438 let line_index = self
439 .line_starts
440 .partition_point(|&start| start <= byte)
441 .saturating_sub(1);
442 let line_start = self.line_starts.get(line_index).copied().unwrap_or(0);
443 let column_chars = self
444 .source
445 .get(line_start..byte)
446 .map_or(0, |prefix| prefix.chars().count());
447 (
448 u32::try_from(line_index + 1).unwrap_or(u32::MAX),
449 u32::try_from(column_chars + 1).unwrap_or(u32::MAX),
450 )
451 }
452
453 fn visit(&mut self, cst: Node<'_>, out: &mut Vec<IrNode>, depth: usize) {
455 if depth >= MAX_IR_DEPTH {
456 self.emit_depth_error(cst, out);
457 return;
458 }
459
460 match self.mapping.classify(&cst) {
461 Mapping::Emit(shape) => {
462 let name = self
463 .mapping
464 .node_name(&cst, self.source)
465 .map(|text| self.interner.intern(text));
466 let node = self.build_node(shape, name, cst, depth);
467 out.push(node);
468 }
469 Mapping::Native(kind) => {
470 let shape = Shape::Native(self.interner.intern(kind));
471 let node = self.build_node(shape, None, cst, depth);
472 out.push(node);
473 }
474 Mapping::ExprStmt => {
475 if self.inner_expression_emits(cst) {
476 self.visit_children(cst, out, depth);
478 } else {
479 let node = self.build_node(Shape::ExprStmt, None, cst, depth);
480 out.push(node);
481 }
482 }
483 Mapping::Error => {
484 self.error_ranges.push(node_range(&cst));
485 let node = self.build_node(Shape::Error, None, cst, depth);
488 out.push(node);
489 }
490 Mapping::Transparent => self.visit_children(cst, out, depth),
491 }
492 }
493
494 fn visit_children(&mut self, cst: Node<'_>, out: &mut Vec<IrNode>, depth: usize) {
495 let mut cursor = cst.walk();
496 let children: Vec<Node<'_>> = cst.named_children(&mut cursor).collect();
497 for child in children {
498 self.visit(child, out, depth + 1);
499 }
500 }
501
502 fn build_node(
504 &mut self,
505 shape: Shape,
506 name: Option<Lexeme>,
507 cst: Node<'_>,
508 depth: usize,
509 ) -> IrNode {
510 let mut children = Vec::new();
511 self.visit_children(cst, &mut children, depth);
512 let range = node_range(&cst);
513 IrNode {
514 shape,
515 name,
516 token_start: self.token_index_at(range.start),
517 token_end: self.token_index_at(range.end),
518 range,
519 children,
520 }
521 }
522
523 fn emit_depth_error(&mut self, cst: Node<'_>, out: &mut Vec<IrNode>) {
525 let range = node_range(&cst);
526 self.depth_truncated = true;
527 self.error_ranges.push(range);
528 out.push(IrNode {
529 shape: Shape::Error,
530 name: None,
531 token_start: self.token_index_at(range.start),
532 token_end: self.token_index_at(range.end),
533 range,
534 children: Vec::new(),
535 });
536 }
537
538 fn token_index_at(&self, byte: usize) -> usize {
540 self.token_starts.partition_point(|&start| start < byte)
541 }
542
543 fn inner_expression_emits(&self, stmt: Node<'_>) -> bool {
546 let mut cursor = stmt.walk();
547 stmt.named_children(&mut cursor)
548 .find(|child| child.kind() != COMMENT_KIND)
549 .is_some_and(|inner| {
550 matches!(
551 self.mapping.classify(&inner),
552 Mapping::Emit(_) | Mapping::Native(_) | Mapping::Error
553 )
554 })
555 }
556}
557
558#[derive(Debug, Clone, Copy, Default)]
560pub struct CMapping;
561
562impl IrMapping for CMapping {
563 fn classify(&self, node: &Node<'_>) -> Mapping {
564 classify_c(node)
565 }
566}
567
568#[derive(Debug, Clone, Copy, Default)]
570pub struct CStructuralFrontend;
571
572impl StructuralFrontend for CStructuralFrontend {
573 fn language(&self) -> Language {
574 Language::C
575 }
576
577 fn frontend_version(&self) -> &'static str {
578 STRUCTURAL_FRONTEND_VERSION
579 }
580
581 fn parse(&self, source: &str) -> SyntaxIrFile {
582 let grammar = tree_sitter::Language::from(tree_sitter_c::LANGUAGE);
583 parse_to_ir(
584 source,
585 &grammar,
586 &CMapping,
587 Language::C,
588 STRUCTURAL_FRONTEND_VERSION,
589 )
590 }
591}
592
593#[cfg(test)]
594#[allow(clippy::unwrap_used, clippy::expect_used)]
595mod tests;