use crate::ast::{Program, Statement};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone)]
pub struct CallGraph {
edges: HashMap<String, HashSet<String>>,
words: HashSet<String>,
recursive_sccs: Vec<HashSet<String>>,
}
impl CallGraph {
pub fn build(program: &Program) -> Self {
let mut edges: HashMap<String, HashSet<String>> = HashMap::new();
let words: HashSet<String> = program.words.iter().map(|w| w.name.clone()).collect();
for word in &program.words {
let callees = extract_calls(&word.body, &words);
edges.insert(word.name.clone(), callees);
}
let mut graph = CallGraph {
edges,
words,
recursive_sccs: Vec::new(),
};
graph.recursive_sccs = graph.find_sccs();
graph
}
pub fn is_recursive(&self, word: &str) -> bool {
self.recursive_sccs.iter().any(|scc| scc.contains(word))
}
pub fn is_self_recursive(&self, word: &str) -> bool {
self.edges
.get(word)
.is_some_and(|callees| callees.contains(word))
}
pub fn are_mutually_recursive(&self, word1: &str, word2: &str) -> bool {
self.recursive_sccs
.iter()
.any(|scc| scc.contains(word1) && scc.contains(word2))
}
pub fn recursive_cycles(&self) -> &[HashSet<String>] {
&self.recursive_sccs
}
pub fn callees(&self, word: &str) -> Option<&HashSet<String>> {
self.edges.get(word)
}
fn find_sccs(&self) -> Vec<HashSet<String>> {
let mut state = TarjanState::new();
for word in &self.words {
if !state.indices.contains_key(word) {
self.tarjan_visit(word, &mut state);
}
}
state
.sccs
.into_iter()
.filter(|scc| {
if scc.len() > 1 {
true
} else if scc.len() == 1 {
let word = scc.iter().next().expect("scc.len() == 1");
self.edges
.get(word)
.map(|callees| callees.contains(word))
.unwrap_or(false)
} else {
false
}
})
.collect()
}
fn tarjan_visit(&self, word: &str, state: &mut TarjanState) {
let index = state.index_counter;
state.index_counter += 1;
state.indices.insert(word.to_string(), index);
state.lowlinks.insert(word.to_string(), index);
state.stack.push(word.to_string());
state.on_stack.insert(word.to_string());
if let Some(callees) = self.edges.get(word) {
for callee in callees {
if !self.words.contains(callee) {
continue;
}
if !state.indices.contains_key(callee) {
self.tarjan_visit(callee, state);
let callee_lowlink = *state
.lowlinks
.get(callee)
.expect("Tarjan invariant: callee was just visited");
state.relax_lowlink(word, callee_lowlink);
} else if state.on_stack.contains(callee) {
let callee_index = *state
.indices
.get(callee)
.expect("Tarjan invariant: on-stack callee is indexed");
state.relax_lowlink(word, callee_index);
}
}
}
if state.lowlinks.get(word) == state.indices.get(word) {
let mut scc = HashSet::new();
loop {
let w = state
.stack
.pop()
.expect("Tarjan invariant: stack non-empty until root");
state.on_stack.remove(&w);
scc.insert(w.clone());
if w == word {
break;
}
}
state.sccs.push(scc);
}
}
}
struct TarjanState {
index_counter: usize,
stack: Vec<String>,
on_stack: HashSet<String>,
indices: HashMap<String, usize>,
lowlinks: HashMap<String, usize>,
sccs: Vec<HashSet<String>>,
}
impl TarjanState {
fn new() -> Self {
TarjanState {
index_counter: 0,
stack: Vec::new(),
on_stack: HashSet::new(),
indices: HashMap::new(),
lowlinks: HashMap::new(),
sccs: Vec::new(),
}
}
fn relax_lowlink(&mut self, word: &str, candidate: usize) {
let lowlink = self
.lowlinks
.get_mut(word)
.expect("Tarjan invariant: word has a lowlink");
*lowlink = (*lowlink).min(candidate);
}
}
fn extract_calls(statements: &[Statement], known_words: &HashSet<String>) -> HashSet<String> {
let mut calls = HashSet::new();
extract_each(statements, known_words, &mut calls);
calls
}
fn extract_each(
statements: &[Statement],
known_words: &HashSet<String>,
calls: &mut HashSet<String>,
) {
for stmt in statements {
extract_calls_from_statement(stmt, known_words, calls);
}
}
fn extract_calls_from_statement(
stmt: &Statement,
known_words: &HashSet<String>,
calls: &mut HashSet<String>,
) {
match stmt {
Statement::WordCall { name, .. } => {
if known_words.contains(name) {
calls.insert(name.clone());
}
}
Statement::If {
then_branch,
else_branch,
span: _,
} => {
extract_each(then_branch, known_words, calls);
if let Some(else_stmts) = else_branch {
extract_each(else_stmts, known_words, calls);
}
}
Statement::Quotation { body, .. } => {
extract_each(body, known_words, calls);
}
Statement::Match { arms, span: _ } => {
for arm in arms {
extract_each(&arm.body, known_words, calls);
}
}
Statement::IntLiteral(_)
| Statement::FloatLiteral(_)
| Statement::BoolLiteral(_)
| Statement::StringLiteral(_)
| Statement::Symbol(_) => {}
}
}
#[cfg(test)]
mod tests;