Skip to main content

geographdb_core/
cfg_store.rs

1//! CFG Store - Combines octree spatial indexing with persistent storage
2//!
3//! This module provides a high-level API for storing and querying
4//! Control Flow Graph (CFG) blocks using 3D spatial indexing.
5
6use anyhow::{Context, Result};
7use std::collections::HashMap;
8use std::path::Path;
9
10use crate::spatial::{BoundingBox, Octree};
11use crate::storage::{EdgeRec, MetadataRec, NodePoint, NodeRec, StorageManager};
12use glam::Vec3;
13
14/// CFG Block metadata stored separately from spatial index
15#[derive(Debug, Clone)]
16pub struct CfgBlockMetadata {
17    pub block_kind: String,
18    pub terminator: String,
19    pub byte_start: u64,
20    pub byte_end: u64,
21    pub start_line: u64,
22    pub start_col: u64,
23    pub end_line: u64,
24    pub end_col: u64,
25}
26
27/// CFG Block representation for spatial storage
28#[derive(Debug, Clone)]
29pub struct CfgBlock {
30    pub id: u64,
31    pub function_id: i64,
32    pub block_kind: String,
33    pub terminator: String,
34    pub byte_start: u64,
35    pub byte_end: u64,
36    pub start_line: u64,
37    pub start_col: u64,
38    pub end_line: u64,
39    pub end_col: u64,
40    pub dominator_depth: u32,
41    pub loop_nesting: u32,
42    pub branch_count: u32,
43}
44
45impl CfgBlock {
46    pub fn to_node_point(&self) -> NodePoint {
47        NodePoint {
48            id: self.id,
49            x: self.dominator_depth as f32,
50            y: self.loop_nesting as f32,
51            z: self.function_id as f32, // Store function_id in z for spatial queries
52        }
53    }
54
55    pub fn to_node_rec(&self, _assigned_id: u64) -> NodeRec {
56        use crate::spatial::MortonEncode;
57        // Debug: write to file
58        if let Ok(mut f) = std::fs::OpenOptions::new()
59            .create(true)
60            .append(true)
61            .open("/tmp/cfg_debug.log")
62        {
63            use std::io::Write;
64            let _ = writeln!(
65                f,
66                "to_node_rec: id={}, function_id={}, z={}",
67                self.id, self.function_id, self.function_id as f32
68            );
69        }
70        NodeRec {
71            id: self.id, // Use logical block ID for LSTS version tracking
72            morton_code: <()>::encode_morton(
73                self.dominator_depth,
74                self.loop_nesting,
75                self.function_id as u32, // Use function_id for spatial indexing (cast to u32)
76            ),
77            x: self.dominator_depth as f32,
78            y: self.loop_nesting as f32,
79            z: self.function_id as f32, // Store function_id in z for spatial queries
80            edge_off: 0,
81            edge_len: 0,
82            // Store branch_count in flags field for later retrieval
83            flags: self.branch_count,
84            begin_ts: 0,
85            end_ts: 0,
86            tx_id: 0,
87            visibility: 1,
88            _padding: [0; 7],
89        }
90    }
91
92    pub fn to_metadata(&self) -> CfgBlockMetadata {
93        CfgBlockMetadata {
94            block_kind: self.block_kind.clone(),
95            terminator: self.terminator.clone(),
96            byte_start: self.byte_start,
97            byte_end: self.byte_end,
98            start_line: self.start_line,
99            start_col: self.start_col,
100            end_line: self.end_line,
101            end_col: self.end_col,
102        }
103    }
104
105    pub fn from_metadata(
106        id: u64,
107        function_id: i64,
108        meta: &CfgBlockMetadata,
109        dominator_depth: u32,
110        loop_nesting: u32,
111        branch_count: u32,
112    ) -> Self {
113        CfgBlock {
114            id,
115            function_id,
116            block_kind: meta.block_kind.clone(),
117            terminator: meta.terminator.clone(),
118            byte_start: meta.byte_start,
119            byte_end: meta.byte_end,
120            start_line: meta.start_line,
121            start_col: meta.start_col,
122            end_line: meta.end_line,
123            end_col: meta.end_col,
124            dominator_depth,
125            loop_nesting,
126            branch_count,
127        }
128    }
129}
130
131/// CFG Store - combines octree + storage for efficient CFG queries
132pub struct CfgStore {
133    /// Octree spatial index (public for persistence)
134    pub octree: Octree,
135    storage: StorageManager,
136    function_bounds: HashMap<i64, BoundingBox>,
137    /// Sidecar metadata storage indexed by storage-assigned block ID
138    metadata: HashMap<u64, CfgBlockMetadata>,
139    /// Map function_id to storage-assigned block IDs
140    function_blocks: HashMap<i64, Vec<u64>>,
141}
142
143impl CfgStore {
144    pub fn create(path: &Path) -> Result<Self> {
145        let storage = StorageManager::create(path).context("Failed to create storage")?;
146        let bounds = BoundingBox::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(100.0, 100.0, 100.0));
147        let octree = Octree::new(bounds);
148        Ok(Self {
149            octree,
150            storage,
151            function_bounds: HashMap::new(),
152            metadata: HashMap::new(),
153            function_blocks: HashMap::new(),
154        })
155    }
156
157    pub fn open(path: &Path) -> Result<Self> {
158        let storage = StorageManager::open(path).context("Failed to open storage")?;
159        let bounds = BoundingBox::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(100.0, 100.0, 100.0));
160        let mut octree = Octree::new(bounds);
161
162        // Rebuild in-memory data structures from persistent storage
163        let mut metadata = HashMap::new();
164        let mut function_blocks: HashMap<i64, Vec<u64>> = HashMap::new();
165        let mut function_bounds: HashMap<i64, BoundingBox> = HashMap::new();
166
167        // Iterate over all nodes to rebuild metadata and function_blocks
168        for node_id in 0..storage.node_count() as u64 {
169            if let Some(node) = storage.get_node(node_id) {
170                // function_id stored in z coordinate during to_node_rec()
171                let function_id = node.z as i64;
172
173                // Load metadata from persistent storage
174                let block_metadata = if let Some(meta_rec) = storage.get_metadata(node_id) {
175                    CfgBlockMetadata {
176                        block_kind: meta_rec.get_block_kind(),
177                        terminator: meta_rec.get_terminator(),
178                        byte_start: meta_rec.byte_start,
179                        byte_end: meta_rec.byte_end,
180                        start_line: meta_rec.start_line,
181                        start_col: meta_rec.start_col,
182                        end_line: meta_rec.end_line,
183                        end_col: meta_rec.end_col,
184                    }
185                } else {
186                    // Fallback to unknown for databases without metadata section
187                    CfgBlockMetadata {
188                        block_kind: "unknown".to_string(),
189                        terminator: "unknown".to_string(),
190                        byte_start: 0,
191                        byte_end: 0,
192                        start_line: 0,
193                        start_col: 0,
194                        end_line: 0,
195                        end_col: 0,
196                    }
197                };
198                metadata.insert(node_id, block_metadata);
199
200                // Add to function_blocks mapping
201                function_blocks
202                    .entry(function_id)
203                    .or_default()
204                    .push(node_id);
205
206                // Update function bounds
207                let min = Vec3::new(node.x, node.y, node.z);
208                let max = Vec3::new(node.x, node.y, node.z);
209                let entry = function_bounds
210                    .entry(function_id)
211                    .or_insert(BoundingBox::new(min, max));
212                entry.min = entry.min.min(min);
213                entry.max = entry.max.max(max);
214
215                // Rebuild octree spatial index
216                let point = NodePoint {
217                    id: node_id,
218                    x: node.x,
219                    y: node.y,
220                    z: node.z,
221                };
222                octree.insert(point);
223            }
224        }
225
226        Ok(Self {
227            octree,
228            storage,
229            function_bounds,
230            metadata,
231            function_blocks,
232        })
233    }
234
235    pub fn insert_block(&mut self, block: CfgBlock) -> Result<u64> {
236        let function_id = block.function_id;
237
238        // Insert into storage and get assigned ID
239        let assigned_id = self
240            .storage
241            .insert_node(block.to_node_rec(self.storage.node_count() as u64))
242            .context("Failed to insert into storage")?;
243
244        // Create metadata record for persistent storage and store at the same ID as the node
245        let meta_rec = MetadataRec::from_strings(
246            &block.block_kind,
247            &block.terminator,
248            block.byte_start,
249            block.byte_end,
250            block.start_line,
251            block.start_col,
252            block.end_line,
253            block.end_col,
254        );
255
256        // Persist metadata to disk at the same ID as the node
257        self.storage
258            .insert_metadata_at(assigned_id, meta_rec)
259            .context("Failed to insert metadata")?;
260
261        // Store metadata in memory HashMap for fast access
262        let metadata = block.to_metadata();
263        self.metadata.insert(assigned_id, metadata);
264
265        // Track which blocks belong to which function
266        self.function_blocks
267            .entry(function_id)
268            .or_default()
269            .push(assigned_id);
270
271        self.update_function_bounds(function_id, &block);
272        self.octree.insert(block.to_node_point());
273
274        Ok(assigned_id)
275    }
276
277    pub fn insert_blocks(&mut self, blocks: Vec<CfgBlock>) -> Result<()> {
278        for block in blocks {
279            self.insert_block(block)?;
280        }
281        Ok(())
282    }
283
284    pub fn query_nearby(&self, point: Vec3, radius: f32) -> Vec<NodePoint> {
285        self.octree.query_sphere(point, radius)
286    }
287
288    /// Find the k nearest CFG blocks to a query point.
289    ///
290    /// Returns up to k nearest blocks sorted by ascending distance.
291    /// Each result includes the distance squared for precision comparison.
292    ///
293    /// # Example
294    /// ```ignore
295    /// let nearest = store.query_knn(glam::Vec3::new(1.0, 2.0, 3.0), 5);
296    /// for (node_point, dist_sq) in nearest {
297    ///     println!("Block {} at distance {}", node_point.id, dist_sq.sqrt());
298    /// }
299    /// ```
300    pub fn query_knn(&self, point: Vec3, k: usize) -> Vec<(NodePoint, f32)> {
301        self.octree.query_knn(point, k)
302    }
303
304    pub fn block_count(&self) -> usize {
305        self.storage.node_count()
306    }
307
308    pub fn metadata_count(&self) -> usize {
309        self.storage.metadata_count()
310    }
311
312    pub fn get_stored_metadata(&self, id: u64) -> Option<&crate::storage::MetadataRec> {
313        self.storage.get_metadata(id)
314    }
315
316    /// Flush any pending changes to disk
317    pub fn flush(&mut self) -> Result<()> {
318        self.storage.flush()
319    }
320
321    /// LSTS: Get a block at a specific timestamp (time-travel query)
322    ///
323    /// Returns the CfgBlock version that was visible at the given timestamp.
324    /// This enables querying the CFG as it existed at any point in time.
325    pub fn get_block_at_timestamp(
326        &self,
327        logical_block_id: u64,
328        timestamp: u64,
329    ) -> Option<CfgBlock> {
330        // Use the storage manager's LSTS query to find the right version
331        let _node = self
332            .storage
333            .get_node_at_timestamp(logical_block_id, timestamp)?;
334
335        // Find the storage ID for this version
336        // We need to scan the version index to find which physical storage ID
337        // corresponds to this logical block at this timestamp
338        let versions = self.storage.get_version_history(logical_block_id)?;
339
340        for &version_id in versions {
341            if let Some(node_version) = self.storage.get_node(version_id) {
342                if node_version.begin_ts <= timestamp
343                    && (node_version.end_ts == 0 || node_version.end_ts > timestamp)
344                {
345                    // Found the right version, now get the metadata
346                    if let Some(meta) = self.metadata.get(&version_id) {
347                        // Determine function_id from the metadata or node
348                        // For now, we scan function_blocks to find which function owns this block
349                        let function_id = self
350                            .function_blocks
351                            .iter()
352                            .find(|(_, blocks)| blocks.contains(&version_id))
353                            .map(|(fid, _)| *fid)
354                            .unwrap_or(0);
355
356                        return Some(CfgBlock::from_metadata(
357                            logical_block_id,
358                            function_id,
359                            meta,
360                            node_version.x as u32,
361                            node_version.y as u32,
362                            node_version.z as u32,
363                        ));
364                    }
365                }
366            }
367        }
368
369        None
370    }
371
372    /// Get all blocks for a function WITH FULL METADATA
373    pub fn get_blocks_for_function(&self, function_id: i64) -> Vec<CfgBlock> {
374        let mut blocks = Vec::new();
375
376        // Get block IDs for this function
377        if let Some(block_ids) = self.function_blocks.get(&function_id) {
378            for &block_id in block_ids {
379                // Get spatial data from storage
380                if let Some(node) = self.storage.get_node(block_id) {
381                    // Get metadata from sidecar table
382                    if let Some(meta) = self.metadata.get(&block_id) {
383                        // Use node.id (the logical block ID) instead of storage position
384                        // Note: node.z is function_id, node.flags is branch_count
385                        blocks.push(CfgBlock::from_metadata(
386                            node.id,
387                            function_id,
388                            meta,
389                            node.x as u32, // dominator_depth
390                            node.y as u32, // loop_nesting
391                            node.flags,    // branch_count (stored in flags)
392                        ));
393                    }
394                }
395            }
396        }
397
398        blocks
399    }
400
401    pub fn get_all_edges(&self) -> Vec<EdgeRec> {
402        let mut edges = Vec::new();
403        for i in 0..self.storage.edge_count() {
404            if let Some(edge) = self.storage.get_edge(i as u64) {
405                edges.push(*edge);
406            }
407        }
408        edges
409    }
410    /// Insert an edge into storage
411    pub fn insert_edge(&mut self, edge: EdgeRec) -> Result<u64> {
412        self.storage
413            .insert_edge(edge)
414            .context("Failed to insert edge")
415    }
416
417    pub fn get_edges_for_node(&self, source_idx: u64) -> Vec<EdgeRec> {
418        let mut edges = Vec::new();
419        for i in 0..self.storage.edge_count() {
420            if let Some(edge) = self.storage.get_edge(i as u64) {
421                if edge.src == source_idx {
422                    edges.push(*edge);
423                }
424            }
425        }
426        edges
427    }
428
429    /// Get the set of function IDs that have blocks in this store
430    ///
431    /// Returns all function IDs that have at least one CFG block.
432    /// This is useful for vacuum operations to determine which functions are present.
433    pub fn get_function_ids(&self) -> std::collections::HashSet<i64> {
434        self.function_blocks.keys().copied().collect()
435    }
436
437    /// Get block IDs for a specific function
438    ///
439    /// Returns the block IDs that belong to the given function, or None if the
440    /// function has no blocks in this store.
441    pub fn get_block_ids_for_function(&self, function_id: i64) -> Option<&[u64]> {
442        self.function_blocks.get(&function_id).map(|v| v.as_slice())
443    }
444
445    /// Get count of blocks for a specific function
446    pub fn count_blocks_for_function(&self, function_id: i64) -> usize {
447        self.function_blocks
448            .get(&function_id)
449            .map(|v| v.len())
450            .unwrap_or(0)
451    }
452
453    /// Get the total edge count in storage
454    pub fn edge_count(&self) -> usize {
455        self.storage.edge_count()
456    }
457
458    fn update_function_bounds(&mut self, function_id: i64, block: &CfgBlock) {
459        let pos = Vec3::new(
460            block.dominator_depth as f32,
461            block.loop_nesting as f32,
462            block.branch_count as f32,
463        );
464        self.function_bounds
465            .entry(function_id)
466            .and_modify(|bounds| {
467                bounds.min = bounds.min.min(pos);
468                bounds.max = bounds.max.max(pos);
469            })
470            .or_insert_with(|| {
471                BoundingBox::new(
472                    pos - Vec3::new(5.0, 5.0, 5.0),
473                    pos + Vec3::new(5.0, 5.0, 5.0),
474                )
475            });
476    }
477}
478#[cfg(test)]
479mod tests {
480    use super::*;
481    use crate::algorithms::astar::CfgGraphNode;
482    use crate::algorithms::dominance::compute_dominance;
483    use tempfile::tempdir;
484
485    #[test]
486    fn test_cfg_store_create() {
487        let temp_dir = tempdir().unwrap();
488        let db_path = temp_dir.path().join("test_cfg.db");
489        let result = CfgStore::create(&db_path);
490        assert!(result.is_ok(), "Should create CFG store");
491    }
492
493    /// Layer 1 Test: Verify dominator depths are computed correctly from CFG structure
494    /// This test will FAIL initially because we need to integrate dominator analysis
495    /// with CfgBlock coordinate computation
496    #[test]
497    fn test_dominator_depth_computation_from_cfg() {
498        // Create a simple CFG: Entry -> A -> B -> C
499        //                           \--> D ----/
500        // Dominator depths should be:
501        // Entry: 0 (dominates itself)
502        // A: 1 (dominated by Entry)
503        // B: 2 (dominated by Entry, A)
504        // C: 3 (dominated by Entry, A, B) OR 2 (dominated by Entry, D) - depending on path
505        // D: 1 (dominated by Entry)
506
507        let nodes = vec![
508            CfgGraphNode {
509                id: 0,
510                x: 0.0,
511                y: 0.0,
512                z: 0.0,
513                successors: vec![1, 4],
514            }, // Entry -> A, D
515            CfgGraphNode {
516                id: 1,
517                x: 1.0,
518                y: 0.0,
519                z: 0.0,
520                successors: vec![2],
521            }, // A -> B
522            CfgGraphNode {
523                id: 2,
524                x: 2.0,
525                y: 0.0,
526                z: 0.0,
527                successors: vec![3],
528            }, // B -> C
529            CfgGraphNode {
530                id: 3,
531                x: 3.0,
532                y: 0.0,
533                z: 0.0,
534                successors: vec![],
535            }, // C (exit)
536            CfgGraphNode {
537                id: 4,
538                x: 1.0,
539                y: 1.0,
540                z: 0.0,
541                successors: vec![3],
542            }, // D -> C
543        ];
544
545        let dom_result = compute_dominance(&nodes, 0);
546
547        // Layer 1: Basic dominator computation succeeded
548        assert!(
549            dom_result.dominators.contains_key(&0),
550            "Layer 1: Entry should have dominators"
551        );
552        assert!(
553            dom_result.dominators.contains_key(&3),
554            "Layer 1: Exit block C should have dominators"
555        );
556
557        // Layer 2: Verify dominator depths are reasonable
558        let entry_depth = dom_result.dominators.get(&0).unwrap().len() as u32;
559        let a_depth = dom_result.dominators.get(&1).unwrap().len() as u32;
560        let b_depth = dom_result.dominators.get(&2).unwrap().len() as u32;
561        let c_depth = dom_result.dominators.get(&3).unwrap().len() as u32;
562        let d_depth = dom_result.dominators.get(&4).unwrap().len() as u32;
563
564        assert_eq!(
565            entry_depth, 1,
566            "Layer 2: Entry should have depth 1 (only itself)"
567        );
568        assert!(
569            a_depth >= 2,
570            "Layer 2: A should have depth >= 2 (Entry + A)"
571        );
572        assert!(b_depth >= a_depth, "Layer 2: B depth should be >= A depth");
573        assert!(
574            c_depth >= 2,
575            "Layer 2: C should have at least entry + self dominators"
576        );
577        assert!(
578            d_depth >= 2,
579            "Layer 2: D should have depth >= 2 (Entry + D)"
580        );
581
582        // Layer 3: Verify CfgBlock coordinates would be computed correctly
583        // This validates that the dominator depth maps to X coordinate
584        let temp_dir = tempdir().unwrap();
585        let db_path = temp_dir.path().join("test_dominator.db");
586        let mut store = CfgStore::create(&db_path).unwrap();
587
588        // Insert blocks with computed dominator depths
589        let blocks = vec![
590            CfgBlock {
591                id: 0,
592                function_id: 1,
593                block_kind: "entry".to_string(),
594                terminator: "fallthrough".to_string(),
595                byte_start: 0,
596                byte_end: 10,
597                start_line: 1,
598                start_col: 0,
599                end_line: 1,
600                end_col: 10,
601                dominator_depth: entry_depth,
602                loop_nesting: 0,
603                branch_count: 0,
604            },
605            CfgBlock {
606                id: 1,
607                function_id: 1,
608                block_kind: "block".to_string(),
609                terminator: "fallthrough".to_string(),
610                byte_start: 10,
611                byte_end: 20,
612                start_line: 2,
613                start_col: 0,
614                end_line: 2,
615                end_col: 10,
616                dominator_depth: a_depth,
617                loop_nesting: 0,
618                branch_count: 0,
619            },
620            CfgBlock {
621                id: 2,
622                function_id: 1,
623                block_kind: "block".to_string(),
624                terminator: "fallthrough".to_string(),
625                byte_start: 20,
626                byte_end: 30,
627                start_line: 3,
628                start_col: 0,
629                end_line: 3,
630                end_col: 10,
631                dominator_depth: b_depth,
632                loop_nesting: 0,
633                branch_count: 0,
634            },
635        ];
636
637        for block in &blocks {
638            store.insert_block(block.clone()).unwrap();
639        }
640
641        // Retrieve and verify coordinates
642        let retrieved = store.get_blocks_for_function(1);
643        assert_eq!(retrieved.len(), 3, "Layer 3: Should retrieve all 3 blocks");
644
645        // Verify dominator depths are preserved in spatial coordinates
646        let entry_block = retrieved.iter().find(|b| b.block_kind == "entry").unwrap();
647        assert_eq!(
648            entry_block.dominator_depth, 1,
649            "Layer 3: Entry dominator depth should be preserved"
650        );
651
652        // Layer 4: Spatial query should respect dominator structure
653        // Blocks with similar dominator depths should be spatially close
654        let nearby = store.query_nearby(glam::Vec3::new(entry_depth as f32, 0.0, 0.0), 5.0);
655        assert!(
656            !nearby.is_empty(),
657            "Layer 4: Should find entry block by dominator depth coordinate"
658        );
659    }
660
661    /// Phase D Layer 3 Test: Octree persistence integration with CfgStore
662    ///
663    /// Tests that CfgStore can save and restore its octree spatial index.
664    /// This enables fast startup by avoiding octree rebuild from scratch.
665    #[test]
666    fn test_cfg_store_octree_persistence() {
667        let temp_dir = tempdir().unwrap();
668        let db_path = temp_dir.path().join("test_octree_persist.db");
669
670        // Create CfgStore and insert blocks
671        let mut store = CfgStore::create(&db_path).unwrap();
672
673        let blocks = vec![
674            CfgBlock {
675                id: 1,
676                function_id: 42,
677                block_kind: "entry".to_string(),
678                terminator: "fallthrough".to_string(),
679                byte_start: 0,
680                byte_end: 50,
681                start_line: 1,
682                start_col: 0,
683                end_line: 5,
684                end_col: 10,
685                dominator_depth: 0,
686                loop_nesting: 0,
687                branch_count: 0,
688            },
689            CfgBlock {
690                id: 2,
691                function_id: 42,
692                block_kind: "if".to_string(),
693                terminator: "conditional".to_string(),
694                byte_start: 50,
695                byte_end: 100,
696                start_line: 6,
697                start_col: 4,
698                end_line: 10,
699                end_col: 15,
700                dominator_depth: 1,
701                loop_nesting: 0,
702                branch_count: 1,
703            },
704            CfgBlock {
705                id: 3,
706                function_id: 42,
707                block_kind: "loop".to_string(),
708                terminator: "jump".to_string(),
709                byte_start: 100,
710                byte_end: 150,
711                start_line: 11,
712                start_col: 4,
713                end_line: 15,
714                end_col: 20,
715                dominator_depth: 2,
716                loop_nesting: 1,
717                branch_count: 2,
718            },
719        ];
720
721        for block in &blocks {
722            store.insert_block(block.clone()).unwrap();
723        }
724
725        // Verify blocks are queryable
726        let nearby_before = store.query_nearby(glam::Vec3::new(1.0, 0.0, 42.0), 5.0);
727        assert!(
728            !nearby_before.is_empty(),
729            "Should find blocks before persistence"
730        );
731
732        // Serialize octree
733        let octree_bytes = store.octree.to_bytes().expect("Should serialize octree");
734        assert!(
735            !octree_bytes.is_empty(),
736            "Serialized octree should not be empty"
737        );
738
739        // Create new CfgStore and restore octree
740        let mut store2 = CfgStore::create(&db_path).unwrap();
741        store2.octree = Octree::from_bytes(&octree_bytes).expect("Should deserialize octree");
742
743        // Verify blocks are still queryable after restore
744        let nearby_after = store2.query_nearby(glam::Vec3::new(1.0, 0.0, 42.0), 5.0);
745        assert!(
746            !nearby_after.is_empty(),
747            "Should find blocks after octree restore"
748        );
749        assert_eq!(
750            nearby_before.len(),
751            nearby_after.len(),
752            "Should find same number of blocks"
753        );
754    }
755
756    /// Phase D Layer 4 Test: Telemetry for octree operations
757    ///
758    /// Verifies loop guards prevent excessive recursion in octree operations.
759    #[test]
760    fn test_cfg_store_function_index_roundtrip() {
761        let temp_dir = tempdir().unwrap();
762        let db_path = temp_dir.path().join("test_function_roundtrip.db");
763
764        // Create store and insert blocks for function 42
765        let mut store = CfgStore::create(&db_path).unwrap();
766        let block = CfgBlock {
767            id: 1,
768            function_id: 42,
769            block_kind: "entry".to_string(),
770            terminator: "fallthrough".to_string(),
771            byte_start: 0,
772            byte_end: 50,
773            start_line: 1,
774            start_col: 0,
775            end_line: 5,
776            end_col: 10,
777            dominator_depth: 0,
778            loop_nesting: 0,
779            branch_count: 0,
780        };
781        store.insert_block(block.clone()).unwrap();
782        assert_eq!(store.count_blocks_for_function(42), 1);
783
784        // Open a fresh store and verify the function index is rebuilt
785        let store2 = CfgStore::open(&db_path).unwrap();
786        assert_eq!(
787            store2.count_blocks_for_function(42),
788            1,
789            "function_blocks should be rebuilt from storage on open"
790        );
791    }
792    #[test]
793    #[cfg(feature = "telemetry")]
794    fn test_octree_telemetry_loop_guard() {
795        use crate::telemetry::LoopGuard;
796
797        // Test that loop guard catches excessive iterations
798        let guard = LoopGuard::new("octree_traversal", 100);
799
800        // Simulate octree traversal
801        for i in 0..150 {
802            if i < 100 {
803                assert!(guard.check().is_ok(), "Should allow iteration {}", i);
804            } else {
805                assert!(guard.check().is_err(), "Should fail at iteration {}", i);
806                break;
807            }
808        }
809    }
810}