Skip to main content

forgekit_core/cfg/
mod.rs

1//! CFG module - Control flow graph analysis.
2//!
3//! This module provides CFG operations via Mirage integration.
4
5mod dominators;
6mod paths;
7mod test_cfg;
8mod types;
9
10pub use dominators::DominatorTree;
11pub use paths::{Path, PathBuilder};
12pub use test_cfg::TestCfg;
13pub use types::Loop;
14
15use crate::error::Result;
16use crate::storage::UnifiedGraphStore;
17use crate::types::{BlockId, SymbolId};
18use std::sync::Arc;
19
20#[derive(Clone)]
21pub struct CfgModule {
22    store: Arc<UnifiedGraphStore>,
23}
24
25#[derive(Clone, Debug)]
26pub struct FunctionCfg {
27    pub symbol_id: SymbolId,
28    pub name: String,
29    pub cfg: TestCfg,
30}
31
32impl CfgModule {
33    pub(crate) fn new(store: Arc<UnifiedGraphStore>) -> Self {
34        Self { store }
35    }
36
37    pub async fn index(&self) -> Result<()> {
38        Ok(())
39    }
40
41    pub async fn extract_function_cfg(
42        &self,
43        _file_path: &std::path::Path,
44        function_name: &str,
45    ) -> Result<Option<TestCfg>> {
46        if !self.store.db_path.exists() {
47            return Ok(None);
48        }
49        let conn = rusqlite::Connection::open(&self.store.db_path)
50            .map_err(|e| crate::error::ForgeError::DatabaseError(format!("Open db: {}", e)))?;
51        let entity_id: Option<i64> = conn
52            .query_row(
53                "SELECT id FROM graph_entities WHERE name = ?1
54                 AND kind IN ('fn', 'function') LIMIT 1",
55                rusqlite::params![function_name],
56                |row| row.get(0),
57            )
58            .ok();
59        match entity_id {
60            Some(id) => load_test_cfg(&self.store.db_path, id),
61            None => Ok(None),
62        }
63    }
64
65    pub fn paths(&self, function: SymbolId) -> PathBuilder {
66        PathBuilder {
67            function: Some(function),
68            store: Some(Arc::clone(&self.store)),
69            ..PathBuilder::default()
70        }
71    }
72
73    pub async fn dominators(&self, function: SymbolId) -> Result<DominatorTree> {
74        if let Some(cfg) = load_test_cfg(&self.store.db_path, function.0)? {
75            let dom_tree = cfg.compute_dominators();
76            return Ok(dom_tree);
77        }
78
79        let cfg = TestCfg::chain(0, 5);
80        Ok(cfg.compute_dominators())
81    }
82
83    pub async fn loops(&self, function: SymbolId) -> Result<Vec<Loop>> {
84        if let Some(cfg) = load_test_cfg(&self.store.db_path, function.0)? {
85            return Ok(cfg.detect_loops());
86        }
87
88        let cfg = TestCfg::simple_loop();
89        let loops = cfg.detect_loops();
90
91        Ok(loops)
92    }
93
94    pub async fn detect_cycles(&self) -> Result<CycleReport> {
95        if !self.store.db_path.exists() {
96            return Ok(CycleReport { cycles: Vec::new() });
97        }
98        mirage::forge::detect_cycles(&self.store.db_path)
99            .map(|report| CycleReport {
100                cycles: report
101                    .cycles
102                    .into_iter()
103                    .map(|c| CallCycle {
104                        members: c,
105                        depth: 0,
106                    })
107                    .collect(),
108            })
109            .map_err(|e| {
110                crate::error::ForgeError::DatabaseError(format!("Cycle detection failed: {}", e))
111            })
112    }
113
114    pub async fn dead_symbols(&self, entry_symbol: &str) -> Result<Vec<DeadSymbol>> {
115        if !self.store.db_path.exists() {
116            return Ok(Vec::new());
117        }
118        mirage::forge::find_dead_symbols(entry_symbol, &self.store.db_path)
119            .map(|symbols| {
120                symbols
121                    .into_iter()
122                    .map(|d| DeadSymbol {
123                        name: d.name,
124                        kind: d.kind,
125                        file_path: d.file_path,
126                    })
127                    .collect()
128            })
129            .map_err(|e| {
130                crate::error::ForgeError::DatabaseError(format!(
131                    "Dead symbol analysis failed: {}",
132                    e
133                ))
134            })
135    }
136
137    pub async fn reachable_symbols(&self, symbol_id: &str) -> Result<Vec<DeadSymbol>> {
138        if !self.store.db_path.exists() {
139            return Ok(Vec::new());
140        }
141        mirage::forge::reachable_symbols(symbol_id, &self.store.db_path)
142            .map(|symbols| {
143                symbols
144                    .into_iter()
145                    .map(|s| DeadSymbol {
146                        name: s.name,
147                        kind: s.kind,
148                        file_path: s.file_path,
149                    })
150                    .collect()
151            })
152            .map_err(|e| {
153                crate::error::ForgeError::DatabaseError(format!(
154                    "Reachability analysis failed: {}",
155                    e
156                ))
157            })
158    }
159
160    pub async fn callees(
161        &self,
162        function_name: &str,
163        file_filter: Option<&str>,
164    ) -> Result<Vec<i64>> {
165        if !self.store.db_path.exists() {
166            return Ok(Vec::new());
167        }
168        mirage::forge::get_callees(function_name, file_filter, &self.store.db_path).map_err(|e| {
169            crate::error::ForgeError::DatabaseError(format!("Callee lookup failed: {}", e))
170        })
171    }
172
173    pub async fn resolve_function(&self, name: &str, file_filter: Option<&str>) -> Result<i64> {
174        if !self.store.db_path.exists() {
175            return Err(crate::error::ForgeError::DatabaseError(
176                "graph DB not found".to_string(),
177            ));
178        }
179        mirage::forge::resolve_function(name, file_filter, &self.store.db_path).map_err(|e| {
180            crate::error::ForgeError::DatabaseError(format!("Function resolution failed: {}", e))
181        })
182    }
183
184    pub async fn database_status(&self) -> Result<Option<DatabaseStatus>> {
185        if !self.store.db_path.exists() {
186            return Ok(None);
187        }
188        mirage::forge::database_status(&self.store.db_path)
189            .map(|status| {
190                Some(DatabaseStatus {
191                    cfg_blocks: status.cfg_blocks,
192                    cfg_paths: status.cfg_paths,
193                    cfg_dominators: status.cfg_dominators,
194                    mirage_schema_version: status.mirage_schema_version,
195                    magellan_schema_version: status.magellan_schema_version,
196                })
197            })
198            .map_err(|e| {
199                crate::error::ForgeError::DatabaseError(format!("Status check failed: {}", e))
200            })
201    }
202}
203
204#[derive(Debug, Clone)]
205pub struct CycleReport {
206    pub cycles: Vec<CallCycle>,
207}
208
209#[derive(Debug, Clone)]
210pub struct CallCycle {
211    pub members: Vec<String>,
212    pub depth: usize,
213}
214
215#[derive(Debug, Clone)]
216pub struct DeadSymbol {
217    pub name: String,
218    pub kind: String,
219    pub file_path: String,
220}
221
222#[derive(Debug, Clone)]
223pub struct DatabaseStatus {
224    pub cfg_blocks: i64,
225    pub cfg_paths: i64,
226    pub cfg_dominators: i64,
227    pub mirage_schema_version: i32,
228    pub magellan_schema_version: i32,
229}
230
231fn load_test_cfg(
232    db_path: &std::path::Path,
233    function_id: i64,
234) -> crate::error::Result<Option<TestCfg>> {
235    use rusqlite::{params, Connection};
236
237    let graph_db = db_path;
238    if !graph_db.exists() {
239        return Ok(None);
240    }
241
242    let backend = match mirage::Backend::detect_and_open(graph_db) {
243        Ok(b) => b,
244        Err(_) => return Ok(None),
245    };
246
247    let blocks = match backend.get_cfg_blocks(function_id) {
248        Ok(b) => b,
249        Err(_) => return Ok(None),
250    };
251
252    if blocks.is_empty() {
253        return Ok(None);
254    }
255
256    let entry = BlockId(blocks[0].id);
257    let mut cfg = TestCfg::new(entry);
258
259    let mut has_real_edges = false;
260    if let Ok(conn) = Connection::open(graph_db) {
261        let query = r#"
262            SELECT source_idx, target_idx, edge_type
263            FROM cfg_edges
264            WHERE function_id = ?1
265            ORDER BY id
266        "#;
267        if let Ok(mut stmt) = conn.prepare(query) {
268            if let Ok(rows) = stmt.query_map(params![function_id], |row| {
269                Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?))
270            }) {
271                for row in rows.flatten() {
272                    let src = BlockId(blocks.get(row.0 as usize).map(|b| b.id).unwrap_or(row.0));
273                    let dst = BlockId(blocks.get(row.1 as usize).map(|b| b.id).unwrap_or(row.1));
274                    cfg.add_edge(src, dst);
275                    has_real_edges = true;
276                }
277            }
278        }
279    }
280
281    if !has_real_edges {
282        for i in 0..blocks.len().saturating_sub(1) {
283            cfg.add_edge(BlockId(blocks[i].id), BlockId(blocks[i + 1].id));
284        }
285    }
286
287    for b in &blocks {
288        if b.terminator == "return" || b.terminator == "throw" {
289            cfg.add_exit(BlockId(b.id));
290        }
291    }
292    if cfg.exits.is_empty() {
293        if let Some(last) = blocks.last() {
294            cfg.add_exit(BlockId(last.id));
295        }
296    }
297
298    Ok(Some(cfg))
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304    use crate::storage::BackendKind;
305
306    #[tokio::test]
307    async fn test_cfg_module_creation() {
308        let store = Arc::new(
309            UnifiedGraphStore::open(tempfile::tempdir().unwrap().path(), BackendKind::SQLite)
310                .await
311                .unwrap(),
312        );
313        let module = CfgModule::new(Arc::clone(&store));
314
315        assert_eq!(module.store.db_path(), store.db_path());
316    }
317
318    #[tokio::test]
319    async fn test_path_builder_filters() {
320        let store = Arc::new(
321            UnifiedGraphStore::open(tempfile::tempdir().unwrap().path(), BackendKind::SQLite)
322                .await
323                .unwrap(),
324        );
325
326        let dummy_module = CfgModule {
327            store: Arc::clone(&store),
328        };
329
330        let builder = dummy_module.paths(SymbolId(1)).normal_only().max_length(10);
331
332        assert!(builder.normal_only);
333        assert_eq!(builder.max_length, Some(10));
334    }
335
336    #[tokio::test]
337    async fn test_dominators_basic() {
338        let store = Arc::new(
339            UnifiedGraphStore::open(tempfile::tempdir().unwrap().path(), BackendKind::SQLite)
340                .await
341                .unwrap(),
342        );
343        let module = CfgModule::new(store);
344
345        let doms = module.dominators(SymbolId(1)).await.unwrap();
346        assert_eq!(doms.root, BlockId(0));
347        assert_eq!(doms.dominators.len(), 4);
348    }
349
350    #[tokio::test]
351    async fn test_loops_detection() {
352        let store = Arc::new(
353            UnifiedGraphStore::open(tempfile::tempdir().unwrap().path(), BackendKind::SQLite)
354                .await
355                .unwrap(),
356        );
357        let module = CfgModule::new(store);
358
359        let loops = module.loops(SymbolId(1)).await.unwrap();
360        assert_eq!(loops.len(), 1);
361    }
362
363    #[tokio::test]
364    async fn test_paths_execute_no_db_returns_synthetic() {
365        let store = Arc::new(
366            UnifiedGraphStore::open(tempfile::tempdir().unwrap().path(), BackendKind::SQLite)
367                .await
368                .unwrap(),
369        );
370        let module = CfgModule::new(store);
371
372        let paths = module.paths(SymbolId(1)).execute().await.unwrap();
373        assert!(!paths.is_empty());
374        assert_eq!(paths[0].blocks.len(), 1);
375        assert_eq!(paths[0].blocks[0], BlockId(1));
376    }
377
378    fn make_cfg_fixture_db(db_path: &std::path::Path, fn_name: &str) -> i64 {
379        use crate::storage::{open_graph, GraphConfig, NodeSpec};
380        let config = GraphConfig::sqlite();
381        let backend = open_graph(db_path, &config).unwrap();
382        let node = NodeSpec {
383            kind: "fn".to_string(),
384            name: fn_name.to_string(),
385            file_path: Some("src/lib.rs".to_string()),
386            data: serde_json::Value::Null,
387        };
388        let entity_id = backend.insert_node(node).unwrap();
389        drop(backend);
390
391        let conn = rusqlite::Connection::open(db_path).unwrap();
392        conn.execute_batch(
393            "CREATE TABLE IF NOT EXISTS cfg_blocks (
394                id INTEGER PRIMARY KEY AUTOINCREMENT,
395                function_id INTEGER NOT NULL,
396                kind TEXT NOT NULL,
397                terminator TEXT NOT NULL,
398                byte_start INTEGER,
399                byte_end INTEGER,
400                start_line INTEGER,
401                start_col INTEGER,
402                end_line INTEGER,
403                end_col INTEGER,
404                coord_x INTEGER DEFAULT 0,
405                coord_y INTEGER DEFAULT 0,
406                coord_z INTEGER DEFAULT 0,
407                cfg_condition TEXT
408            );",
409        )
410        .unwrap();
411        conn.execute(
412            "INSERT INTO cfg_blocks
413                (function_id, kind, terminator, byte_start, byte_end,
414                 start_line, start_col, end_line, end_col)
415             VALUES (?1, 'entry', 'return', 0, 50, 1, 0, 5, 0)",
416            rusqlite::params![entity_id],
417        )
418        .unwrap();
419        entity_id
420    }
421
422    #[tokio::test]
423    async fn test_extract_function_cfg_finds_fn_entity() {
424        let dir = tempfile::tempdir().unwrap();
425        let db_path = dir.path().join("cfg_test.db");
426
427        make_cfg_fixture_db(&db_path, "compute_flow");
428
429        let store = Arc::new(
430            crate::storage::UnifiedGraphStore::open_with_path(
431                dir.path(),
432                &db_path,
433                BackendKind::SQLite,
434            )
435            .await
436            .unwrap(),
437        );
438        let module = CfgModule::new(store);
439
440        let result = module
441            .extract_function_cfg(std::path::Path::new("src/lib.rs"), "compute_flow")
442            .await
443            .unwrap();
444
445        assert!(
446            result.is_some(),
447            "should return Some(cfg) when function entity and CFG blocks exist"
448        );
449    }
450}