use codehelion_core::discovery::Language;
use codehelion_core::frontend::{
Lexeme, LexemeInterner, LiteralKind, SourceSpan, Token, TokenKind,
};
use codehelion_core::ir::{
ByteRange, IR_SCHEMA_VERSION, IrNode, MAX_IR_DEPTH, Shape, StructuralFrontend, SyntaxIrFile,
};
use tree_sitter::{Node, Parser};
pub const STRUCTURAL_FRONTEND_VERSION: &str = "c-ir-v1";
const ATOMIC_TOKEN_KINDS: &[&str] = &[
"string_literal",
"char_literal",
"system_lib_string",
"raw_string_literal",
];
const COMMENT_KIND: &str = "comment";
#[derive(Debug, Clone)]
pub enum Mapping {
Emit(Shape),
Native(&'static str),
ExprStmt,
Error,
Transparent,
}
pub trait IrMapping {
fn classify(&self, node: &Node<'_>) -> Mapping;
fn node_name<'s>(&self, node: &Node<'_>, source: &'s str) -> Option<&'s str> {
c_family_node_name(node, source)
}
fn token_kind(&self, kind: &str, is_named: bool, text: &str) -> TokenKind {
classify_token(kind, is_named, text)
}
}
#[must_use]
pub fn classify_c(node: &Node<'_>) -> Mapping {
match node.kind() {
"function_definition" => Mapping::Emit(Shape::Function),
"compound_statement" => Mapping::Emit(Shape::Block),
"for_statement" | "while_statement" | "do_statement" => Mapping::Emit(Shape::Loop),
"if_statement" => Mapping::Emit(Shape::Branch),
"switch_statement" => Mapping::Emit(Shape::Match),
"case_statement" => Mapping::Emit(Shape::MatchArm),
"call_expression" => Mapping::Emit(Shape::Call),
"assignment_expression" => Mapping::Emit(Shape::Assign),
"declaration" => Mapping::Emit(Shape::VarDecl),
"return_statement" => Mapping::Emit(Shape::Return),
"break_statement" => Mapping::Emit(Shape::Break),
"continue_statement" => Mapping::Emit(Shape::Continue),
"expression_statement" => Mapping::ExprStmt,
"preproc_def" | "preproc_function_def" => Mapping::Emit(Shape::MacroDef),
"goto_statement" => Mapping::Native("goto_statement"),
"preproc_if" | "preproc_ifdef" | "preproc_else" | "preproc_elif" | "preproc_elifdef" => {
Mapping::Native(node.kind())
}
"struct_specifier" | "union_specifier" | "enum_specifier" => record_mapping(node),
"ERROR" => Mapping::Error,
_ => Mapping::Transparent,
}
}
#[must_use]
pub fn record_mapping(node: &Node<'_>) -> Mapping {
if node.child_by_field_name("body").is_some() {
Mapping::Emit(Shape::Record)
} else {
Mapping::Transparent
}
}
#[must_use]
pub fn classify_token(kind: &str, is_named: bool, text: &str) -> TokenKind {
match kind {
"identifier"
| "field_identifier"
| "type_identifier"
| "statement_identifier"
| "namespace_identifier" => TokenKind::Identifier,
"primitive_type" | "sized_type_specifier" | "auto" | "this" => TokenKind::Keyword,
"null" => {
if text == "nullptr" {
TokenKind::Keyword
} else {
TokenKind::Identifier
}
}
"number_literal" => TokenKind::Literal(number_literal_kind(text)),
"string_literal" | "system_lib_string" | "raw_string_literal" => {
TokenKind::Literal(LiteralKind::String)
}
"char_literal" => TokenKind::Literal(LiteralKind::Char),
"true" | "false" => TokenKind::Literal(LiteralKind::Bool),
_ if !is_named => {
if !kind.is_empty() && kind.chars().all(|c| c.is_ascii_alphabetic() || c == '_') {
TokenKind::Keyword
} else {
TokenKind::Punctuation
}
}
_ => TokenKind::Unknown,
}
}
fn number_literal_kind(text: &str) -> LiteralKind {
let hex = text.starts_with("0x") || text.starts_with("0X");
let float = text.contains('.')
|| if hex {
text.contains(['p', 'P'])
} else {
text.contains(['e', 'E']) || text.ends_with(['f', 'F'])
};
if float {
LiteralKind::Float
} else {
LiteralKind::Integer
}
}
#[must_use]
pub fn c_family_node_name<'s>(node: &Node<'_>, source: &'s str) -> Option<&'s str> {
match node.kind() {
"function_definition" => {
declarator_identifier(node.child_by_field_name("declarator")?, source)
}
"struct_specifier"
| "union_specifier"
| "enum_specifier"
| "class_specifier"
| "preproc_def"
| "preproc_function_def" => node_text(&node.child_by_field_name("name")?, source),
_ => None,
}
}
fn declarator_identifier<'s>(declarator: Node<'_>, source: &'s str) -> Option<&'s str> {
let mut current = declarator;
loop {
match current.kind() {
"identifier" | "field_identifier" | "type_identifier" | "operator_name"
| "destructor_name" => return node_text(¤t, source),
"qualified_identifier" => current = current.child_by_field_name("name")?,
"pointer_declarator"
| "function_declarator"
| "parenthesized_declarator"
| "reference_declarator" => {
current = current
.child_by_field_name("declarator")
.or_else(|| current.named_child(0))?;
}
_ => return None,
}
}
}
fn node_text<'s>(node: &Node<'_>, source: &'s str) -> Option<&'s str> {
source.get(node.start_byte()..node.end_byte())
}
fn node_range(node: &Node<'_>) -> ByteRange {
ByteRange {
start: node.start_byte(),
end: node.end_byte(),
}
}
#[must_use]
pub fn parse_to_ir(
source: &str,
grammar: &tree_sitter::Language,
mapping: &dyn IrMapping,
language: Language,
frontend_version: &'static str,
) -> SyntaxIrFile {
let mut parser = Parser::new();
let tree = if parser.set_language(grammar).is_ok() {
parser.parse(source, None)
} else {
None
};
let Some(tree) = tree else {
return SyntaxIrFile {
language,
frontend_version,
ir_schema_version: IR_SCHEMA_VERSION,
tokens: Vec::new(),
roots: Vec::new(),
diagnostics: Vec::new(),
error_ranges: vec![ByteRange {
start: 0,
end: source.len(),
}],
depth_truncated: false,
test_module: false,
};
};
let root = tree.root_node();
let mut builder = IrBuilder::new(source, mapping);
builder.collect_tokens(root);
let mut roots = Vec::new();
builder.visit(root, &mut roots, 0);
builder
.error_ranges
.sort_unstable_by_key(|range| (range.start, range.end));
builder.error_ranges.dedup();
SyntaxIrFile {
language,
frontend_version,
ir_schema_version: IR_SCHEMA_VERSION,
tokens: builder.tokens,
roots,
diagnostics: Vec::new(),
error_ranges: builder.error_ranges,
depth_truncated: builder.depth_truncated,
test_module: false,
}
}
struct IrBuilder<'s, 'm> {
source: &'s str,
mapping: &'m dyn IrMapping,
interner: LexemeInterner,
tokens: Vec<Token>,
token_starts: Vec<usize>,
line_starts: Vec<usize>,
error_ranges: Vec<ByteRange>,
depth_truncated: bool,
}
impl<'s, 'm> IrBuilder<'s, 'm> {
fn new(source: &'s str, mapping: &'m dyn IrMapping) -> Self {
let mut line_starts = vec![0];
for (index, byte) in source.bytes().enumerate() {
if byte == b'\n' {
line_starts.push(index + 1);
}
}
Self {
source,
mapping,
interner: LexemeInterner::new(),
tokens: Vec::new(),
token_starts: Vec::new(),
line_starts,
error_ranges: Vec::new(),
depth_truncated: false,
}
}
fn collect_tokens(&mut self, root: Node<'_>) {
let mut cursor = root.walk();
loop {
let node = cursor.node();
let kind = node.kind();
let descend = kind != COMMENT_KIND
&& !ATOMIC_TOKEN_KINDS.contains(&kind)
&& node.child_count() > 0;
if descend && cursor.goto_first_child() {
continue;
}
if !descend && kind != COMMENT_KIND {
if node.is_missing() {
self.error_ranges.push(node_range(&node));
} else if node.end_byte() > node.start_byte() {
self.emit_token(&node);
}
}
loop {
if cursor.goto_next_sibling() {
break;
}
if !cursor.goto_parent() {
return;
}
}
}
}
fn emit_token(&mut self, node: &Node<'_>) {
let start_byte = node.start_byte();
let end_byte = node.end_byte();
let text = node_text(node, self.source).unwrap_or("");
let kind = self.mapping.token_kind(node.kind(), node.is_named(), text);
let (start_line, start_column) = self.line_column(start_byte);
let text = self.interner.intern(text);
self.token_starts.push(start_byte);
self.tokens.push(Token {
kind,
text,
span: SourceSpan {
start_byte,
end_byte,
start_line,
start_column,
},
});
}
fn line_column(&self, byte: usize) -> (u32, u32) {
let line_index = self
.line_starts
.partition_point(|&start| start <= byte)
.saturating_sub(1);
let line_start = self.line_starts.get(line_index).copied().unwrap_or(0);
let column_chars = self
.source
.get(line_start..byte)
.map_or(0, |prefix| prefix.chars().count());
(
u32::try_from(line_index + 1).unwrap_or(u32::MAX),
u32::try_from(column_chars + 1).unwrap_or(u32::MAX),
)
}
fn visit(&mut self, cst: Node<'_>, out: &mut Vec<IrNode>, depth: usize) {
if depth >= MAX_IR_DEPTH {
self.emit_depth_error(cst, out);
return;
}
match self.mapping.classify(&cst) {
Mapping::Emit(shape) => {
let name = self
.mapping
.node_name(&cst, self.source)
.map(|text| self.interner.intern(text));
let node = self.build_node(shape, name, cst, depth);
out.push(node);
}
Mapping::Native(kind) => {
let shape = Shape::Native(self.interner.intern(kind));
let node = self.build_node(shape, None, cst, depth);
out.push(node);
}
Mapping::ExprStmt => {
if self.inner_expression_emits(cst) {
self.visit_children(cst, out, depth);
} else {
let node = self.build_node(Shape::ExprStmt, None, cst, depth);
out.push(node);
}
}
Mapping::Error => {
self.error_ranges.push(node_range(&cst));
let node = self.build_node(Shape::Error, None, cst, depth);
out.push(node);
}
Mapping::Transparent => self.visit_children(cst, out, depth),
}
}
fn visit_children(&mut self, cst: Node<'_>, out: &mut Vec<IrNode>, depth: usize) {
let mut cursor = cst.walk();
let children: Vec<Node<'_>> = cst.named_children(&mut cursor).collect();
for child in children {
self.visit(child, out, depth + 1);
}
}
fn build_node(
&mut self,
shape: Shape,
name: Option<Lexeme>,
cst: Node<'_>,
depth: usize,
) -> IrNode {
let mut children = Vec::new();
self.visit_children(cst, &mut children, depth);
let range = node_range(&cst);
IrNode {
shape,
name,
token_start: self.token_index_at(range.start),
token_end: self.token_index_at(range.end),
range,
children,
}
}
fn emit_depth_error(&mut self, cst: Node<'_>, out: &mut Vec<IrNode>) {
let range = node_range(&cst);
self.depth_truncated = true;
self.error_ranges.push(range);
out.push(IrNode {
shape: Shape::Error,
name: None,
token_start: self.token_index_at(range.start),
token_end: self.token_index_at(range.end),
range,
children: Vec::new(),
});
}
fn token_index_at(&self, byte: usize) -> usize {
self.token_starts.partition_point(|&start| start < byte)
}
fn inner_expression_emits(&self, stmt: Node<'_>) -> bool {
let mut cursor = stmt.walk();
stmt.named_children(&mut cursor)
.find(|child| child.kind() != COMMENT_KIND)
.is_some_and(|inner| {
matches!(
self.mapping.classify(&inner),
Mapping::Emit(_) | Mapping::Native(_) | Mapping::Error
)
})
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct CMapping;
impl IrMapping for CMapping {
fn classify(&self, node: &Node<'_>) -> Mapping {
classify_c(node)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct CStructuralFrontend;
impl StructuralFrontend for CStructuralFrontend {
fn language(&self) -> Language {
Language::C
}
fn frontend_version(&self) -> &'static str {
STRUCTURAL_FRONTEND_VERSION
}
fn parse(&self, source: &str) -> SyntaxIrFile {
let grammar = tree_sitter::Language::from(tree_sitter_c::LANGUAGE);
parse_to_ir(
source,
&grammar,
&CMapping,
Language::C,
STRUCTURAL_FRONTEND_VERSION,
)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests;