Skip to main content

mirage/
forge.rs

1//! High-level API for external consumers (forge, agents, tool integrations).
2//!
3//! This module provides convenience functions that wrap `MirageDb`, `Backend`,
4//! and `MagellanBridge` into single-call functions for common operations.
5//!
6//! # Example
7//!
8//! ```no_run
9//! use std::path::Path;
10//! use mirage_analyzer::forge;
11//!
12//! let db = Path::new(".magellan/myproject.db");
13//!
14//! // Get CFG for a function by name
15//! let cfg = forge::get_function_cfg("my_function", None, db).unwrap();
16//! println!("{} blocks, {} edges", cfg.blocks.len(), cfg.edges.len());
17//!
18//! // Detect cycles in the call graph
19//! let cycles = forge::detect_cycles(db).unwrap();
20//! println!("{} cycles found", cycles.cycles.len());
21//!
22//! // Find dead symbols (unreachable from entry points)
23//! let dead = forge::find_dead_symbols("main", db).unwrap();
24//! println!("{} dead symbols", dead.len());
25//! ```
26
27use crate::analysis::MagellanBridge;
28use crate::cfg::{Cfg, Path as CfgPath};
29use crate::storage::{Backend, MirageDb};
30use anyhow::Result;
31use std::path::Path;
32
33/// CFG result for a function.
34#[derive(Debug, Clone)]
35pub struct FunctionCfgResult {
36    /// Function name.
37    pub name: String,
38    /// Function ID in the database.
39    pub function_id: i64,
40    /// The control flow graph.
41    pub cfg: Cfg,
42}
43
44/// Cycle report from call graph analysis.
45#[derive(Debug, Clone)]
46pub struct CycleReport {
47    /// Detected cycles (each cycle is a list of symbol names).
48    pub cycles: Vec<Vec<String>>,
49}
50
51/// Dead symbol information.
52#[derive(Debug, Clone)]
53pub struct DeadSymbolInfo {
54    /// Symbol name.
55    pub name: String,
56    /// Symbol kind.
57    pub kind: String,
58    /// File path.
59    pub file_path: String,
60}
61
62/// Resolve a function name to its ID, optionally filtered by file.
63///
64/// Uses `SymbolNavigator` for name-based resolution.
65///
66/// # Errors
67///
68/// Returns an error if the function cannot be found.
69pub fn resolve_function(name: &str, file_filter: Option<&str>, db_path: &Path) -> Result<i64> {
70    let db = MirageDb::open(db_path)?;
71    db.resolve_function_name_with_file(name, file_filter)
72}
73
74/// Get the CFG for a function by name.
75///
76/// Resolves the function name to an ID, loads CFG blocks, and constructs
77/// a complete control flow graph.
78///
79/// # Arguments
80///
81/// * `name` - Function name (or numeric ID as string)
82/// * `file_filter` - Optional file path substring to disambiguate
83/// * `db_path` - Path to the Magellan database
84///
85/// # Errors
86///
87/// Returns an error if the function is not found or has no CFG data.
88pub fn get_function_cfg(
89    name: &str,
90    file_filter: Option<&str>,
91    db_path: &Path,
92) -> Result<FunctionCfgResult> {
93    let db = MirageDb::open(db_path)?;
94    let function_id = db.resolve_function_name_with_file(name, file_filter)?;
95    let cfg = db.load_cfg(function_id)?;
96
97    Ok(FunctionCfgResult {
98        name: name.to_string(),
99        function_id,
100        cfg,
101    })
102}
103
104/// Get cached execution paths for a function.
105///
106/// Returns pre-computed paths if available in the database.
107///
108/// # Errors
109///
110/// Returns an error if the function is not found.
111pub fn get_function_paths(
112    name: &str,
113    file_filter: Option<&str>,
114    db_path: &Path,
115) -> Result<Option<Vec<CfgPath>>> {
116    let backend = Backend::detect_and_open(db_path)?;
117    let db = MirageDb::open(db_path)?;
118    let function_id = db.resolve_function_name_with_file(name, file_filter)?;
119    backend.get_cached_paths(function_id)
120}
121
122/// Get callees (outgoing calls) for a function.
123///
124/// Returns the function IDs of all functions called from the specified function.
125///
126/// # Errors
127///
128/// Returns an error if the function is not found.
129pub fn get_callees(name: &str, file_filter: Option<&str>, db_path: &Path) -> Result<Vec<i64>> {
130    let backend = Backend::detect_and_open(db_path)?;
131    let db = MirageDb::open(db_path)?;
132    let function_id = db.resolve_function_name_with_file(name, file_filter)?;
133    backend.get_callees(function_id)
134}
135
136/// Detect cycles in the call graph.
137///
138/// Uses magellan's cycle detection algorithm to find all strongly connected
139/// components with more than one node.
140///
141/// # Errors
142///
143/// Returns an error if the database cannot be opened.
144pub fn detect_cycles(db_path: &Path) -> Result<CycleReport> {
145    let bridge = MagellanBridge::open(db_path.to_str().unwrap_or("."))?;
146    let result = bridge.detect_cycles()?;
147
148    let cycles = result
149        .cycles
150        .into_iter()
151        .map(|cycle| {
152            cycle
153                .members
154                .into_iter()
155                .filter_map(|info| info.fqn)
156                .collect()
157        })
158        .collect();
159
160    Ok(CycleReport { cycles })
161}
162
163/// Find dead symbols (unreachable from the given entry point).
164///
165/// Computes forward reachability from the entry symbol and returns
166/// all symbols that are NOT reachable.
167///
168/// # Arguments
169///
170/// * `entry_symbol` - The entry point symbol (e.g. "main")
171/// * `db_path` - Path to the Magellan database
172///
173/// # Errors
174///
175/// Returns an error if the database cannot be opened.
176pub fn find_dead_symbols(entry_symbol: &str, db_path: &Path) -> Result<Vec<DeadSymbolInfo>> {
177    let bridge = MagellanBridge::open(db_path.to_str().unwrap_or("."))?;
178    let dead = bridge.dead_symbols(entry_symbol)?;
179
180    Ok(dead
181        .into_iter()
182        .map(|d| DeadSymbolInfo {
183            name: d.symbol.fqn.clone().unwrap_or_default(),
184            kind: d.symbol.kind.clone(),
185            file_path: d.symbol.file_path.clone(),
186        })
187        .collect())
188}
189
190/// Find all symbols reachable from a given symbol (forward reachability).
191///
192/// # Arguments
193///
194/// * `symbol_id` - The BLAKE3 symbol ID to start from
195/// * `db_path` - Path to the Magellan database
196///
197/// # Errors
198///
199/// Returns an error if the database cannot be opened.
200pub fn reachable_symbols(symbol_id: &str, db_path: &Path) -> Result<Vec<DeadSymbolInfo>> {
201    let bridge = MagellanBridge::open(db_path.to_str().unwrap_or("."))?;
202    let reachable = bridge.reachable_symbols(symbol_id)?;
203
204    Ok(reachable
205        .into_iter()
206        .map(|r| DeadSymbolInfo {
207            name: r.fqn.clone().unwrap_or_default(),
208            kind: r.kind.clone(),
209            file_path: r.file_path.clone(),
210        })
211        .collect())
212}
213
214/// Get database status.
215///
216/// Returns basic statistics about the database contents.
217///
218/// # Errors
219///
220/// Returns an error if the database cannot be opened.
221pub fn database_status(db_path: &Path) -> Result<crate::storage::DatabaseStatus> {
222    let db = MirageDb::open(db_path)?;
223    db.status()
224}