use crate::discovery::Language;
use crate::frontend::{Diagnostic, Lexeme, Token};
pub const IR_SCHEMA_VERSION: u32 = 1;
pub const MAX_IR_DEPTH: usize = 500;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct ByteRange {
pub start: usize,
pub end: usize,
}
impl ByteRange {
#[must_use]
pub const fn len(&self) -> usize {
self.end.saturating_sub(self.start)
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.len() == 0
}
#[must_use]
pub const fn contains(&self, other: &Self) -> bool {
self.start <= other.start && other.end <= self.end
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Shape {
Function,
Method,
Closure,
Record,
Impl,
Block,
Loop,
Branch,
Match,
MatchArm,
Call,
Assign,
VarDecl,
Return,
Break,
Continue,
Try,
ExprStmt,
MacroDef,
MacroCall,
Error,
Native(Lexeme),
}
impl Shape {
#[must_use]
pub const fn tag(&self) -> u8 {
match self {
Self::Function => 1,
Self::Method => 2,
Self::Closure => 3,
Self::Record => 4,
Self::Impl => 5,
Self::Block => 6,
Self::Loop => 7,
Self::Branch => 8,
Self::Match => 9,
Self::MatchArm => 10,
Self::Call => 11,
Self::Assign => 12,
Self::VarDecl => 13,
Self::Return => 14,
Self::Break => 15,
Self::Continue => 16,
Self::Try => 17,
Self::ExprStmt => 18,
Self::MacroDef => 19,
Self::MacroCall => 20,
Self::Error => 21,
Self::Native(_) => 22,
}
}
#[must_use]
pub const fn introduces_scope(&self) -> bool {
matches!(
self,
Self::Function
| Self::Method
| Self::Closure
| Self::Block
| Self::Loop
| Self::Branch
| Self::Match
| Self::MatchArm
| Self::Try
)
}
#[must_use]
pub const fn is_statement(&self) -> bool {
matches!(
self,
Self::Loop
| Self::Branch
| Self::Match
| Self::Assign
| Self::VarDecl
| Self::Return
| Self::Break
| Self::Continue
| Self::Try
| Self::ExprStmt
| Self::MacroCall
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IrNode {
pub shape: Shape,
pub name: Option<Lexeme>,
pub token_start: usize,
pub token_end: usize,
pub range: ByteRange,
pub children: Vec<Self>,
}
impl IrNode {
#[must_use]
pub const fn token_len(&self) -> usize {
self.token_end.saturating_sub(self.token_start)
}
pub fn walk(&self, visit: &mut impl FnMut(&Self)) {
let mut pending = vec![self];
while let Some(node) = pending.pop() {
visit(node);
pending.extend(node.children.iter().rev());
}
}
#[must_use]
pub fn statement_summaries(&self, tokens: &[Token]) -> Vec<StatementSummary> {
self.children
.iter()
.filter(|child| child.shape.is_statement() || matches!(child.shape, Shape::Native(_)))
.map(|child| StatementSummary::of(child, tokens))
.collect()
}
}
impl Drop for IrNode {
fn drop(&mut self) {
let mut worklist = std::mem::take(&mut self.children);
while let Some(mut node) = worklist.pop() {
worklist.append(&mut node.children);
}
}
}
pub const SUMMARY_HEAD_TOKENS: usize = 4;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StatementSummary {
pub shape_tag: u8,
pub native_kind: Option<Lexeme>,
pub token_start: usize,
pub token_end: usize,
}
impl StatementSummary {
#[must_use]
pub fn of(node: &IrNode, tokens: &[Token]) -> Self {
let native_kind = match &node.shape {
Shape::Native(kind) => Some(kind.clone()),
_ => None,
};
let token_end = node.token_end.min(tokens.len());
Self {
shape_tag: node.shape.tag(),
native_kind,
token_start: node.token_start.min(token_end),
token_end,
}
}
#[must_use]
pub fn tokens<'a>(&self, tokens: &'a [Token]) -> &'a [Token] {
tokens.get(self.token_start..self.token_end).unwrap_or(&[])
}
}
#[derive(Debug, Clone)]
pub struct SyntaxIrFile {
pub language: Language,
pub frontend_version: &'static str,
pub ir_schema_version: u32,
pub tokens: Vec<Token>,
pub roots: Vec<IrNode>,
pub diagnostics: Vec<Diagnostic>,
pub error_ranges: Vec<ByteRange>,
pub depth_truncated: bool,
pub test_module: bool,
}
impl SyntaxIrFile {
pub fn walk(&self, visit: &mut impl FnMut(&IrNode)) {
for root in &self.roots {
root.walk(visit);
}
}
#[must_use]
pub fn node_count(&self) -> usize {
let mut count = 0;
self.walk(&mut |_| count += 1);
count
}
#[must_use]
pub fn unaccounted_tokens(&self) -> usize {
let mut lost = 0;
self.walk(&mut |node| {
if matches!(node.shape, Shape::Error) {
let recovered: usize = node.children.iter().map(IrNode::token_len).sum();
lost += node.token_len().saturating_sub(recovered);
}
});
lost
}
}
pub trait StructuralFrontend {
fn language(&self) -> Language;
fn frontend_version(&self) -> &'static str;
fn parse(&self, source: &str) -> SyntaxIrFile;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::frontend::{SourceSpan, TokenKind};
fn token(text: &str, start_byte: usize) -> Token {
Token {
kind: TokenKind::Identifier,
text: Lexeme::from(text),
span: SourceSpan {
start_byte,
end_byte: start_byte + text.len(),
start_line: 1,
start_column: 1,
},
}
}
fn node(shape: Shape, token_start: usize, token_end: usize) -> IrNode {
IrNode {
shape,
name: None,
token_start,
token_end,
range: ByteRange {
start: token_start,
end: token_end,
},
children: Vec::new(),
}
}
#[test]
fn dropping_a_deep_tree_is_iterative() {
let mut tree = node(Shape::Block, 0, 0);
for _ in 0..10_000 {
tree = IrNode {
shape: Shape::Block,
name: None,
token_start: 0,
token_end: 0,
range: ByteRange { start: 0, end: 0 },
children: vec![tree],
};
}
drop(tree);
}
#[test]
fn shape_tags_are_distinct_and_stable() {
let shapes = [
Shape::Function,
Shape::Method,
Shape::Closure,
Shape::Record,
Shape::Impl,
Shape::Block,
Shape::Loop,
Shape::Branch,
Shape::Match,
Shape::MatchArm,
Shape::Call,
Shape::Assign,
Shape::VarDecl,
Shape::Return,
Shape::Break,
Shape::Continue,
Shape::Try,
Shape::ExprStmt,
Shape::MacroDef,
Shape::MacroCall,
Shape::Error,
Shape::Native(Lexeme::from("preproc_ifdef")),
];
let mut tags: Vec<u8> = shapes.iter().map(Shape::tag).collect();
tags.sort_unstable();
tags.dedup();
assert_eq!(tags.len(), shapes.len(), "shape tags must be distinct");
assert_eq!(Shape::Function.tag(), 1);
assert_eq!(Shape::Native(Lexeme::from("x")).tag(), 22);
}
#[test]
fn native_nodes_share_a_tag_but_keep_their_kind() {
let a = Shape::Native(Lexeme::from("preproc_ifdef"));
let b = Shape::Native(Lexeme::from("using_declaration"));
assert_eq!(a.tag(), b.tag());
assert_ne!(a, b, "the kind name still distinguishes native shapes");
}
#[test]
fn scope_and_statement_tables_are_consistent() {
assert!(Shape::Function.introduces_scope());
assert!(Shape::Block.introduces_scope());
assert!(!Shape::Call.introduces_scope());
assert!(!Shape::Record.introduces_scope());
assert!(Shape::Return.is_statement());
assert!(Shape::MacroCall.is_statement());
assert!(!Shape::Function.is_statement(), "items are not statements");
assert!(!Shape::Block.is_statement());
}
#[test]
fn byte_range_arithmetic_guards_malformed_input() {
let range = ByteRange { start: 10, end: 20 };
assert_eq!(range.len(), 10);
assert!(!range.is_empty());
assert!(range.contains(&ByteRange { start: 12, end: 18 }));
assert!(!range.contains(&ByteRange { start: 5, end: 18 }));
let malformed = ByteRange { start: 20, end: 10 };
assert_eq!(malformed.len(), 0);
assert!(malformed.is_empty());
}
#[test]
fn statement_summaries_take_statement_children_in_order() {
let tokens: Vec<Token> = ["let", "x", "=", "f", "(", ")", "return", "x"]
.iter()
.enumerate()
.map(|(i, text)| token(text, i * 8))
.collect();
let mut block = node(Shape::Block, 0, 8);
block.children = vec![
node(Shape::VarDecl, 0, 6),
node(Shape::Function, 0, 0), node(Shape::Return, 6, 8),
];
let summaries = block.statement_summaries(&tokens);
assert_eq!(summaries.len(), 2);
assert!(
summaries.iter().all(|s| s.native_kind.is_none()),
"no native statements in this block"
);
assert_eq!(summaries[0].shape_tag, Shape::VarDecl.tag());
let text = |summary: &StatementSummary| -> Vec<String> {
summary
.tokens(&tokens)
.iter()
.map(|token| token.text.as_str().to_string())
.collect()
};
assert_eq!(
text(&summaries[0]),
vec!["let", "x", "=", "f", "(", ")"],
"the span covers the whole statement, not just its head"
);
assert_eq!(summaries[1].shape_tag, Shape::Return.tag());
assert_eq!(text(&summaries[1]), vec!["return", "x"]);
}
#[test]
fn native_children_count_as_statements_in_position() {
let tokens = vec![token("goto", 0), token("fail", 8)];
let mut block = node(Shape::Block, 0, 2);
block.children = vec![IrNode {
shape: Shape::Native(Lexeme::from("goto_statement")),
name: None,
token_start: 0,
token_end: 2,
range: ByteRange { start: 0, end: 12 },
children: Vec::new(),
}];
let summaries = block.statement_summaries(&tokens);
assert_eq!(summaries.len(), 1);
assert_eq!(
summaries[0].native_kind,
Some(Lexeme::from("goto_statement"))
);
}
#[test]
fn summary_of_out_of_bounds_token_range_is_empty_not_panicking() {
let tokens = vec![token("x", 0)];
let stray = node(Shape::ExprStmt, 5, 9);
let summary = StatementSummary::of(&stray, &tokens);
assert!(summary.tokens(&tokens).is_empty());
}
#[test]
fn walk_visits_every_node_pre_order() {
let mut root = node(Shape::Function, 0, 10);
let mut block = node(Shape::Block, 1, 9);
block.children = vec![node(Shape::Return, 2, 4)];
root.children = vec![block];
let file = SyntaxIrFile {
language: Language::Rust,
frontend_version: "test-v1",
ir_schema_version: IR_SCHEMA_VERSION,
tokens: Vec::new(),
roots: vec![root],
diagnostics: Vec::new(),
error_ranges: Vec::new(),
depth_truncated: false,
test_module: false,
};
let mut seen = Vec::new();
file.walk(&mut |n| seen.push(n.shape.tag()));
assert_eq!(
seen,
vec![
Shape::Function.tag(),
Shape::Block.tag(),
Shape::Return.tag()
]
);
assert_eq!(file.node_count(), 3);
}
fn file_of(roots: Vec<IrNode>) -> SyntaxIrFile {
SyntaxIrFile {
language: Language::Rust,
frontend_version: "test-v1",
ir_schema_version: IR_SCHEMA_VERSION,
tokens: Vec::new(),
roots,
diagnostics: Vec::new(),
error_ranges: Vec::new(),
depth_truncated: false,
test_module: false,
}
}
#[test]
fn code_recovered_inside_an_error_node_is_not_counted_as_lost() {
let mut wrapper = node(Shape::Error, 0, 100);
wrapper.children = vec![node(Shape::Function, 3, 60), node(Shape::Function, 60, 100)];
assert_eq!(
file_of(vec![wrapper]).unaccounted_tokens(),
3,
"only the tokens no child accounts for"
);
}
#[test]
fn an_error_node_that_recovered_nothing_loses_all_of_it() {
assert_eq!(
file_of(vec![node(Shape::Error, 0, 40)]).unaccounted_tokens(),
40
);
}
#[test]
fn a_file_the_parser_followed_loses_nothing() {
let mut function = node(Shape::Function, 0, 20);
function.children = vec![node(Shape::Block, 4, 20)];
assert_eq!(file_of(vec![function]).unaccounted_tokens(), 0);
}
#[test]
fn nested_error_nodes_count_their_own_gaps_once() {
let mut inner = node(Shape::Error, 40, 60);
inner.children = vec![node(Shape::Return, 45, 55)];
let mut outer = node(Shape::Error, 0, 100);
outer.children = vec![node(Shape::Function, 0, 40), inner];
assert_eq!(
file_of(vec![outer]).unaccounted_tokens(),
40 + 10,
"the outer's trailing gap plus the inner's own"
);
}
}