use crate::cli::{resolve_db_path, Cli, CycleTypeArg, CyclesArgs, OutputFormat};
use crate::output;
use anyhow::Result;
pub fn cycles(args: &CyclesArgs, cli: &Cli) -> Result<()> {
use crate::analysis::{CycleInfo, EnhancedCycles, LoopInfo, MagellanBridge};
use crate::cfg::detect_natural_loops;
use crate::cfg::load_cfg_from_db;
use crate::storage::MirageDb;
let db_path = resolve_db_path(cli.db.clone())?;
let show_call_graph = args.call_graph || args.both || !args.function_loops;
let show_function_loops = args.function_loops || args.both || !args.call_graph;
let mut call_graph_cycles: Vec<CycleInfo> = if show_call_graph {
match MagellanBridge::open(&db_path) {
Ok(bridge) => match bridge.detect_cycles() {
Ok(report) => report.cycles.iter().map(|c| c.into()).collect(),
Err(e) => {
eprintln!("Warning: Failed to detect call graph cycles: {}", e);
vec![]
}
},
Err(e) => {
eprintln!(
"Warning: Could not open Magellan database for call graph cycles: {}",
e
);
eprintln!("Note: Call graph cycles require a Magellan code graph database");
vec![]
}
}
} else {
vec![]
};
call_graph_cycles.retain(|c| match args.cycle_type {
CycleTypeArg::All => true,
CycleTypeArg::InterFunction => c.cycle_type == "MutualRecursion",
CycleTypeArg::SelfLoop => c.cycle_type == "SelfLoop",
});
let mut function_loops_map: std::collections::HashMap<String, Vec<LoopInfo>> =
std::collections::HashMap::new();
if show_function_loops {
let db = match MirageDb::open(&db_path) {
Ok(db) => db,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::database_not_found(&db_path);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!("Failed to open database: {}", db_path));
output::info("Hint: Run 'magellan watch' to create the database");
std::process::exit(output::EXIT_DATABASE);
}
}
};
if !db.is_sqlite() {
output::error("The 'cycles' command with --function-loops requires SQLite backend.");
output::info("retired binary backend backend is not yet supported for this feature.");
std::process::exit(output::EXIT_USAGE);
}
let mut stmt = match db.conn()?.prepare(
"SELECT name, id FROM graph_entities WHERE kind = 'Symbol' AND json_extract(data, '$.kind') = 'Function'") {
Ok(stmt) => stmt,
Err(e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::new(
"QueryError",
&format!("Failed to query functions: {}", e),
output::E_DATABASE_NOT_FOUND,
);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!("Failed to query functions: {}", e));
std::process::exit(output::EXIT_DATABASE);
}
}
};
let rows_result = stmt.query_map([], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
});
match rows_result {
Ok(rows) => {
for (function_name, function_id) in rows.flatten() {
if let Ok(cfg) = load_cfg_from_db(&db, function_id) {
let natural_loops = detect_natural_loops(&cfg);
if !natural_loops.is_empty() {
let loop_infos: Vec<LoopInfo> = natural_loops
.iter()
.map(|loop_| {
let nesting_level = loop_.nesting_level(&natural_loops);
let body_blocks: Vec<usize> =
loop_.body.iter().map(|&node| cfg[node].id).collect();
LoopInfo {
header: cfg[loop_.header].id,
back_edge_from: cfg[loop_.back_edge.0].id,
body_size: loop_.size(),
nesting_level,
body_blocks,
}
})
.collect();
function_loops_map.insert(function_name, loop_infos);
}
}
}
}
Err(e) => {
eprintln!("Warning: Failed to execute query: {}", e);
}
}
}
let total_cycles =
call_graph_cycles.len() + function_loops_map.values().map(|v| v.len()).sum::<usize>();
let enhanced_cycles = EnhancedCycles {
call_graph_cycles,
function_loops: function_loops_map.clone(),
total_cycles,
};
match cli.output {
OutputFormat::Human => {
println!("Cycle Detection Report");
println!();
if show_call_graph {
println!(
"Call Graph Cycles (Inter-procedural): {}",
enhanced_cycles.call_graph_cycles.len()
);
if enhanced_cycles.call_graph_cycles.is_empty() {
println!(" No call graph cycles detected");
} else {
for (i, cycle) in enhanced_cycles.call_graph_cycles.iter().enumerate() {
println!(" Cycle {}:", i + 1);
println!(" Type: {}", cycle.cycle_type);
println!(" Size: {} symbols", cycle.size);
if args.verbose {
println!(" Members:");
for member in &cycle.members {
println!(" - {}", member);
}
}
}
}
println!();
}
if show_function_loops {
println!(
"Function Loops (Intra-procedural): {} functions with loops",
enhanced_cycles.function_loops.len()
);
if enhanced_cycles.function_loops.is_empty() {
println!(" No natural loops detected in any function");
} else {
for (function_name, loops) in &enhanced_cycles.function_loops {
println!(" Function: {} ({} loops)", function_name, loops.len());
if args.verbose {
for (i, loop_info) in loops.iter().enumerate() {
println!(" Loop {}:", i + 1);
println!(" Header: Block {}", loop_info.header);
println!(
" Back edge from: Block {}",
loop_info.back_edge_from
);
println!(" Body size: {} blocks", loop_info.body_size);
println!(" Nesting level: {}", loop_info.nesting_level);
println!(" Body blocks: {:?}", loop_info.body_blocks);
}
}
}
}
println!();
}
println!("Total cycles: {}", total_cycles);
}
OutputFormat::Json | OutputFormat::Pretty => {
let wrapper = output::JsonResponse::new(enhanced_cycles);
match cli.output {
OutputFormat::Json => println!("{}", wrapper.to_json()),
OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
_ => unreachable!(),
}
}
}
Ok(())
}