impl LeanAstVisitor {
#[must_use]
pub fn new(file_path: &Path) -> Self {
Self {
items: Vec::new(),
_file_path: file_path.to_path_buf(),
namespace: String::new(),
}
}
pub fn analyze_lean_source(mut self, source: &str) -> Result<Vec<AstItem>, String> {
if source.trim().is_empty() {
return Ok(vec![]);
}
for (line_num, line) in source.lines().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with("namespace ") {
let parts: Vec<&str> = trimmed.split_whitespace().collect();
if parts.len() >= 2 {
self.namespace = parts[1].to_string();
}
continue;
}
self.extract_line(trimmed, line_num);
}
Ok(self.items)
}
fn extract_line(&mut self, trimmed: &str, line_num: usize) {
if let Some(item) = self.try_extract_definition(trimmed, line_num) {
self.items.push(item);
return;
}
if let Some(item) = self.try_extract_theorem(trimmed, line_num) {
self.items.push(item);
return;
}
if let Some(item) = self.try_extract_type(trimmed, line_num) {
self.items.push(item);
return;
}
if let Some(item) = self.try_extract_instance(trimmed, line_num) {
self.items.push(item);
return;
}
if let Some(item) = self.try_extract_axiom(trimmed, line_num) {
self.items.push(item);
}
}
fn try_extract_definition(&self, trimmed: &str, line_num: usize) -> Option<AstItem> {
let name = if trimmed.starts_with("def ") {
Self::extract_name_after_keyword(trimmed, "def")
} else if trimmed.starts_with("noncomputable def ") {
Self::extract_name_after_keyword(trimmed, "def")
} else if trimmed.starts_with("abbrev ") {
Self::extract_name_after_keyword(trimmed, "abbrev")
} else if trimmed.starts_with("partial def ") {
Self::extract_name_after_keyword(trimmed, "def")
} else if trimmed.starts_with("private def ") {
Self::extract_name_after_keyword(trimmed, "def")
} else if trimmed.starts_with("protected def ") {
Self::extract_name_after_keyword(trimmed, "def")
} else {
None
}?;
let qualified = self.get_qualified_name(&name);
let visibility = if trimmed.starts_with("private ") {
"private"
} else {
"public"
};
Some(AstItem::Function {
name: qualified,
visibility: visibility.to_string(),
is_async: false,
line: line_num + 1,
})
}
fn try_extract_theorem(&self, trimmed: &str, line_num: usize) -> Option<AstItem> {
let name = if trimmed.starts_with("theorem ") {
Self::extract_name_after_keyword(trimmed, "theorem")
} else if trimmed.starts_with("lemma ") {
Self::extract_name_after_keyword(trimmed, "lemma")
} else if trimmed.starts_with("private theorem ") {
Self::extract_name_after_keyword(trimmed, "theorem")
} else if trimmed.starts_with("private lemma ") {
Self::extract_name_after_keyword(trimmed, "lemma")
} else {
None
}?;
let qualified = self.get_qualified_name(&name);
Some(AstItem::Function {
name: qualified,
visibility: "public".to_string(),
is_async: false,
line: line_num + 1,
})
}
fn try_extract_type(&self, trimmed: &str, line_num: usize) -> Option<AstItem> {
let name = if trimmed.starts_with("structure ") {
Self::extract_name_after_keyword(trimmed, "structure")
} else if trimmed.starts_with("class ") {
Self::extract_name_after_keyword(trimmed, "class")
} else if trimmed.starts_with("inductive ") {
Self::extract_name_after_keyword(trimmed, "inductive")
} else {
None
}?;
let qualified = self.get_qualified_name(&name);
Some(AstItem::Struct {
name: qualified,
visibility: "public".to_string(),
fields_count: 0,
derives: vec![],
line: line_num + 1,
})
}
fn try_extract_instance(&self, trimmed: &str, line_num: usize) -> Option<AstItem> {
if !trimmed.starts_with("instance ") {
return None;
}
let name = Self::extract_name_after_keyword(trimmed, "instance")?;
let qualified = self.get_qualified_name(&name);
Some(AstItem::Function {
name: qualified,
visibility: "public".to_string(),
is_async: false,
line: line_num + 1,
})
}
fn try_extract_axiom(&self, trimmed: &str, line_num: usize) -> Option<AstItem> {
let name = if trimmed.starts_with("axiom ") {
Self::extract_name_after_keyword(trimmed, "axiom")
} else if trimmed.starts_with("opaque ") {
Self::extract_name_after_keyword(trimmed, "opaque")
} else {
None
}?;
let qualified = self.get_qualified_name(&name);
Some(AstItem::Function {
name: qualified,
visibility: "public".to_string(),
is_async: false,
line: line_num + 1,
})
}
fn extract_name_after_keyword(line: &str, keyword: &str) -> Option<String> {
if let Some(pos) = line.find(keyword) {
let after = &line[pos + keyword.len()..].trim_start();
let name = after
.split(|c: char| !c.is_alphanumeric() && c != '_' && c != '.')
.next()
.unwrap_or("");
if !name.is_empty() {
Some(name.to_string())
} else {
None
}
} else {
None
}
}
fn get_qualified_name(&self, name: &str) -> String {
if self.namespace.is_empty() {
name.to_string()
} else {
format!("{}::{}", self.namespace, name)
}
}
}