Skip to main content

cognitum_gate_kernel/
shard.rs

1//! Compact graph shard for tile-local storage
2//!
3//! Implements a fixed-size graph representation optimized for WASM tiles.
4//! Each tile maintains a ~32KB graph shard with deterministic memory layout.
5//!
6//! ## Performance Optimizations
7//!
8//! This module is heavily optimized for hot paths:
9//! - `#[inline(always)]` on all accessors and flag checks
10//! - Unsafe unchecked array access where bounds are pre-validated
11//! - Cache-line aligned structures (64-byte alignment)
12//! - Fixed-point arithmetic (no floats in hot paths)
13//! - Zero allocations in tight loops
14
15#![allow(missing_docs)]
16
17use crate::delta::{FixedWeight, TileEdgeId, TileVertexId};
18use core::mem::size_of;
19
20/// Cache line size for alignment (64 bytes on most modern CPUs)
21const CACHE_LINE_SIZE: usize = 64;
22
23/// Maximum vertices per tile shard
24pub const MAX_SHARD_VERTICES: usize = 256;
25
26/// Maximum edges per tile shard
27pub const MAX_SHARD_EDGES: usize = 1024;
28
29/// Maximum neighbors per vertex (degree limit)
30pub const MAX_DEGREE: usize = 32;
31
32/// Compact edge in shard storage
33///
34/// Size: 8 bytes, cache-friendly for sequential iteration
35#[derive(Debug, Clone, Copy, Default)]
36#[repr(C, align(8))]
37pub struct ShardEdge {
38    /// Source vertex (tile-local)
39    pub source: TileVertexId,
40    /// Target vertex (tile-local)
41    pub target: TileVertexId,
42    /// Edge weight (fixed-point)
43    pub weight: FixedWeight,
44    /// Edge flags
45    pub flags: u16,
46}
47
48impl ShardEdge {
49    /// Edge is active
50    pub const FLAG_ACTIVE: u16 = 0x0001;
51    /// Edge is in current cut
52    pub const FLAG_IN_CUT: u16 = 0x0002;
53    /// Edge is a tree edge in spanning forest
54    pub const FLAG_TREE: u16 = 0x0004;
55    /// Edge crosses tile boundary (ghost edge)
56    pub const FLAG_GHOST: u16 = 0x0008;
57
58    /// Create a new active edge
59    #[inline(always)]
60    pub const fn new(source: TileVertexId, target: TileVertexId, weight: FixedWeight) -> Self {
61        Self {
62            source,
63            target,
64            weight,
65            flags: Self::FLAG_ACTIVE,
66        }
67    }
68
69    /// Check if edge is active
70    ///
71    /// OPTIMIZATION: #[inline(always)] - called in every iteration of edge loops
72    #[inline(always)]
73    pub const fn is_active(&self) -> bool {
74        self.flags & Self::FLAG_ACTIVE != 0
75    }
76
77    /// Check if edge is in cut
78    ///
79    /// OPTIMIZATION: #[inline(always)] - called in mincut algorithms
80    #[inline(always)]
81    pub const fn is_in_cut(&self) -> bool {
82        self.flags & Self::FLAG_IN_CUT != 0
83    }
84
85    /// Check if edge is a tree edge
86    #[inline(always)]
87    pub const fn is_tree(&self) -> bool {
88        self.flags & Self::FLAG_TREE != 0
89    }
90
91    /// Check if edge is a ghost edge
92    #[inline(always)]
93    pub const fn is_ghost(&self) -> bool {
94        self.flags & Self::FLAG_GHOST != 0
95    }
96
97    /// Mark edge as inactive (deleted)
98    #[inline(always)]
99    pub fn deactivate(&mut self) {
100        self.flags &= !Self::FLAG_ACTIVE;
101    }
102
103    /// Mark edge as in cut
104    #[inline(always)]
105    pub fn mark_in_cut(&mut self) {
106        self.flags |= Self::FLAG_IN_CUT;
107    }
108
109    /// Clear cut membership
110    #[inline(always)]
111    pub fn clear_cut(&mut self) {
112        self.flags &= !Self::FLAG_IN_CUT;
113    }
114}
115
116/// Vertex adjacency entry
117///
118/// Size: 8 bytes, aligned for efficient access
119#[derive(Debug, Clone, Copy, Default)]
120#[repr(C, align(8))]
121pub struct VertexEntry {
122    /// Degree (number of active neighbors)
123    pub degree: u8,
124    /// Vertex flags
125    pub flags: u8,
126    /// Component ID (for connectivity tracking)
127    pub component: u16,
128    /// First edge index in adjacency list
129    pub first_edge_idx: u16,
130    /// Reserved for alignment
131    pub _reserved: u16,
132}
133
134impl VertexEntry {
135    /// Vertex is active
136    pub const FLAG_ACTIVE: u8 = 0x01;
137    /// Vertex is on cut boundary
138    pub const FLAG_BOUNDARY: u8 = 0x02;
139    /// Vertex side in partition (0 or 1)
140    pub const FLAG_SIDE: u8 = 0x04;
141    /// Vertex is a ghost (owned by another tile)
142    pub const FLAG_GHOST: u8 = 0x08;
143
144    /// Create a new active vertex
145    #[inline(always)]
146    pub const fn new() -> Self {
147        Self {
148            degree: 0,
149            flags: Self::FLAG_ACTIVE,
150            component: 0,
151            first_edge_idx: 0xFFFF, // Invalid index
152            _reserved: 0,
153        }
154    }
155
156    /// Check if vertex is active
157    ///
158    /// OPTIMIZATION: #[inline(always)] - called in every vertex iteration
159    #[inline(always)]
160    pub const fn is_active(&self) -> bool {
161        self.flags & Self::FLAG_ACTIVE != 0
162    }
163
164    /// Get partition side (0 or 1)
165    ///
166    /// OPTIMIZATION: Branchless version using bit manipulation
167    #[inline(always)]
168    pub const fn side(&self) -> u8 {
169        // Branchless: extract bit 2, shift to position 0
170        (self.flags & Self::FLAG_SIDE) >> 2
171    }
172
173    /// Set partition side
174    ///
175    /// OPTIMIZATION: Branchless flag update
176    #[inline(always)]
177    pub fn set_side(&mut self, side: u8) {
178        // Branchless: clear flag, then set if side != 0
179        self.flags = (self.flags & !Self::FLAG_SIDE) | ((side & 1) << 2);
180    }
181}
182
183/// Adjacency list entry (neighbor + edge reference)
184#[derive(Debug, Clone, Copy, Default)]
185#[repr(C)]
186pub struct AdjEntry {
187    /// Neighbor vertex ID
188    pub neighbor: TileVertexId,
189    /// Edge ID in edge array
190    pub edge_id: TileEdgeId,
191}
192
193/// Compact graph shard for tile-local storage
194///
195/// Memory layout (~32KB total):
196/// - Vertex entries: 256 * 8 = 2KB
197/// - Edge storage: 1024 * 8 = 8KB
198/// - Adjacency lists: 256 * 32 * 4 = 32KB
199/// Total: ~42KB (fits in 64KB tile budget with room for other state)
200///
201/// OPTIMIZATION: Cache-line aligned (64 bytes) for efficient CPU cache usage.
202/// Hot fields (num_vertices, num_edges, status) are grouped together.
203///
204/// Note: Actual size is optimized by packing adjacency lists more efficiently.
205#[repr(C, align(64))]
206pub struct CompactGraph {
207    // === HOT FIELDS (first cache line) ===
208    /// Number of active vertices
209    pub num_vertices: u16,
210    /// Number of active edges
211    pub num_edges: u16,
212    /// Free edge list head (for reuse)
213    pub free_edge_head: u16,
214    /// Graph generation (incremented on structural changes)
215    pub generation: u16,
216    /// Component count
217    pub num_components: u16,
218    /// Status flags
219    pub status: u16,
220    /// Padding to fill cache line
221    _hot_pad: [u8; 52],
222
223    // === COLD FIELDS (subsequent cache lines) ===
224    /// Vertex metadata array
225    pub vertices: [VertexEntry; MAX_SHARD_VERTICES],
226    /// Edge storage array
227    pub edges: [ShardEdge; MAX_SHARD_EDGES],
228    /// Packed adjacency lists
229    /// Layout: for each vertex, up to MAX_DEGREE neighbors
230    pub adjacency: [[AdjEntry; MAX_DEGREE]; MAX_SHARD_VERTICES],
231}
232
233impl Default for CompactGraph {
234    #[inline]
235    fn default() -> Self {
236        Self::new()
237    }
238}
239
240impl CompactGraph {
241    /// Status: graph is valid
242    pub const STATUS_VALID: u16 = 0x0001;
243    /// Status: graph needs recomputation
244    pub const STATUS_DIRTY: u16 = 0x0002;
245    /// Status: graph is connected
246    pub const STATUS_CONNECTED: u16 = 0x0004;
247
248    /// Create a new empty graph
249    pub const fn new() -> Self {
250        Self {
251            num_vertices: 0,
252            num_edges: 0,
253            free_edge_head: 0xFFFF,
254            generation: 0,
255            num_components: 0,
256            status: Self::STATUS_VALID,
257            _hot_pad: [0; 52],
258            vertices: [VertexEntry {
259                degree: 0,
260                flags: 0, // Start inactive
261                component: 0,
262                first_edge_idx: 0xFFFF,
263                _reserved: 0,
264            }; MAX_SHARD_VERTICES],
265            edges: [ShardEdge {
266                source: 0,
267                target: 0,
268                weight: 0,
269                flags: 0,
270            }; MAX_SHARD_EDGES],
271            adjacency: [[AdjEntry {
272                neighbor: 0,
273                edge_id: 0,
274            }; MAX_DEGREE]; MAX_SHARD_VERTICES],
275        }
276    }
277
278    /// Clear the graph
279    pub fn clear(&mut self) {
280        for v in self.vertices.iter_mut() {
281            *v = VertexEntry::new();
282            v.flags = 0; // Mark as inactive
283        }
284        for e in self.edges.iter_mut() {
285            e.flags = 0;
286        }
287        self.num_vertices = 0;
288        self.num_edges = 0;
289        self.free_edge_head = 0xFFFF;
290        self.generation = self.generation.wrapping_add(1);
291        self.num_components = 0;
292        self.status = Self::STATUS_VALID | Self::STATUS_DIRTY;
293    }
294
295    /// Add or activate a vertex
296    pub fn add_vertex(&mut self, v: TileVertexId) -> bool {
297        if v as usize >= MAX_SHARD_VERTICES {
298            return false;
299        }
300
301        let entry = &mut self.vertices[v as usize];
302        if entry.is_active() {
303            return false; // Already active
304        }
305
306        entry.flags = VertexEntry::FLAG_ACTIVE;
307        entry.degree = 0;
308        entry.component = 0;
309        entry.first_edge_idx = 0xFFFF;
310        self.num_vertices += 1;
311        self.status |= Self::STATUS_DIRTY;
312        true
313    }
314
315    /// Remove a vertex (marks as inactive)
316    pub fn remove_vertex(&mut self, v: TileVertexId) -> bool {
317        if v as usize >= MAX_SHARD_VERTICES {
318            return false;
319        }
320
321        let entry = &mut self.vertices[v as usize];
322        if !entry.is_active() {
323            return false;
324        }
325
326        // Deactivate all incident edges
327        for i in 0..entry.degree as usize {
328            let adj = &self.adjacency[v as usize][i];
329            if adj.edge_id < MAX_SHARD_EDGES as u16 {
330                self.edges[adj.edge_id as usize].deactivate();
331                self.num_edges = self.num_edges.saturating_sub(1);
332            }
333        }
334
335        entry.flags = 0;
336        entry.degree = 0;
337        self.num_vertices = self.num_vertices.saturating_sub(1);
338        self.status |= Self::STATUS_DIRTY;
339        self.generation = self.generation.wrapping_add(1);
340        true
341    }
342
343    /// Add an edge between two vertices
344    pub fn add_edge(
345        &mut self,
346        source: TileVertexId,
347        target: TileVertexId,
348        weight: FixedWeight,
349    ) -> Option<TileEdgeId> {
350        // Validate vertices
351        if source as usize >= MAX_SHARD_VERTICES || target as usize >= MAX_SHARD_VERTICES {
352            return None;
353        }
354        if source == target {
355            return None; // No self-loops
356        }
357
358        // Ensure vertices are active
359        if !self.vertices[source as usize].is_active() {
360            self.add_vertex(source);
361        }
362        if !self.vertices[target as usize].is_active() {
363            self.add_vertex(target);
364        }
365
366        // Check degree limits
367        let src_entry = &self.vertices[source as usize];
368        let tgt_entry = &self.vertices[target as usize];
369        if src_entry.degree as usize >= MAX_DEGREE || tgt_entry.degree as usize >= MAX_DEGREE {
370            return None;
371        }
372
373        // Allocate edge slot
374        let edge_id = self.allocate_edge()?;
375
376        // Create edge
377        self.edges[edge_id as usize] = ShardEdge::new(source, target, weight);
378
379        // Update adjacency lists
380        let src_deg = self.vertices[source as usize].degree as usize;
381        self.adjacency[source as usize][src_deg] = AdjEntry {
382            neighbor: target,
383            edge_id,
384        };
385        self.vertices[source as usize].degree += 1;
386
387        let tgt_deg = self.vertices[target as usize].degree as usize;
388        self.adjacency[target as usize][tgt_deg] = AdjEntry {
389            neighbor: source,
390            edge_id,
391        };
392        self.vertices[target as usize].degree += 1;
393
394        self.num_edges += 1;
395        self.status |= Self::STATUS_DIRTY;
396        self.generation = self.generation.wrapping_add(1);
397
398        Some(edge_id)
399    }
400
401    /// Remove an edge
402    pub fn remove_edge(&mut self, source: TileVertexId, target: TileVertexId) -> bool {
403        // Find edge in source's adjacency
404        let edge_id = self.find_edge(source, target);
405        if edge_id.is_none() {
406            return false;
407        }
408        let edge_id = edge_id.unwrap();
409
410        // Deactivate edge
411        self.edges[edge_id as usize].deactivate();
412
413        // Remove from adjacency lists (swap-remove pattern)
414        self.remove_from_adjacency(source, target, edge_id);
415        self.remove_from_adjacency(target, source, edge_id);
416
417        // Add to free list
418        self.free_edge(edge_id);
419
420        self.num_edges = self.num_edges.saturating_sub(1);
421        self.status |= Self::STATUS_DIRTY;
422        self.generation = self.generation.wrapping_add(1);
423        true
424    }
425
426    /// Update edge weight
427    pub fn update_weight(
428        &mut self,
429        source: TileVertexId,
430        target: TileVertexId,
431        new_weight: FixedWeight,
432    ) -> bool {
433        if let Some(edge_id) = self.find_edge(source, target) {
434            self.edges[edge_id as usize].weight = new_weight;
435            self.status |= Self::STATUS_DIRTY;
436            true
437        } else {
438            false
439        }
440    }
441
442    /// Find edge between two vertices
443    ///
444    /// OPTIMIZATION: Uses unsafe unchecked access after bounds validation.
445    /// The adjacency scan is a hot path in graph algorithms.
446    #[inline]
447    pub fn find_edge(&self, source: TileVertexId, target: TileVertexId) -> Option<TileEdgeId> {
448        if source as usize >= MAX_SHARD_VERTICES {
449            return None;
450        }
451
452        // SAFETY: source bounds checked above
453        let entry = unsafe { self.vertices.get_unchecked(source as usize) };
454        if !entry.is_active() {
455            return None;
456        }
457
458        let degree = entry.degree as usize;
459        // SAFETY: source bounds checked, degree <= MAX_DEGREE by invariant
460        let adj_list = unsafe { self.adjacency.get_unchecked(source as usize) };
461
462        for i in 0..degree {
463            // SAFETY: i < degree <= MAX_DEGREE
464            let adj = unsafe { adj_list.get_unchecked(i) };
465            if adj.neighbor == target {
466                return Some(adj.edge_id);
467            }
468        }
469        None
470    }
471
472    /// Find edge between two vertices (unchecked version)
473    ///
474    /// SAFETY: Caller must ensure source < MAX_SHARD_VERTICES and vertex is active
475    #[inline(always)]
476    pub unsafe fn find_edge_unchecked(
477        &self,
478        source: TileVertexId,
479        target: TileVertexId,
480    ) -> Option<TileEdgeId> {
481        unsafe {
482            let entry = self.vertices.get_unchecked(source as usize);
483            let degree = entry.degree as usize;
484            let adj_list = self.adjacency.get_unchecked(source as usize);
485
486            for i in 0..degree {
487                let adj = adj_list.get_unchecked(i);
488                if adj.neighbor == target {
489                    return Some(adj.edge_id);
490                }
491            }
492            None
493        }
494    }
495
496    /// Get edge weight
497    pub fn edge_weight(&self, source: TileVertexId, target: TileVertexId) -> Option<FixedWeight> {
498        self.find_edge(source, target)
499            .map(|eid| self.edges[eid as usize].weight)
500    }
501
502    /// Get vertex degree
503    ///
504    /// OPTIMIZATION: Uses unsafe unchecked access after bounds check
505    #[inline(always)]
506    pub fn degree(&self, v: TileVertexId) -> u8 {
507        if v as usize >= MAX_SHARD_VERTICES {
508            return 0;
509        }
510        // SAFETY: bounds checked above
511        let entry = unsafe { self.vertices.get_unchecked(v as usize) };
512        if entry.is_active() {
513            entry.degree
514        } else {
515            0
516        }
517    }
518
519    /// Get neighbors of a vertex
520    ///
521    /// OPTIMIZATION: Uses unsafe unchecked slice creation after bounds check
522    #[inline]
523    pub fn neighbors(&self, v: TileVertexId) -> &[AdjEntry] {
524        if v as usize >= MAX_SHARD_VERTICES {
525            return &[];
526        }
527        // SAFETY: bounds checked above
528        let entry = unsafe { self.vertices.get_unchecked(v as usize) };
529        if !entry.is_active() {
530            return &[];
531        }
532        let degree = entry.degree as usize;
533        // SAFETY: bounds checked, degree <= MAX_DEGREE by invariant
534        unsafe {
535            self.adjacency
536                .get_unchecked(v as usize)
537                .get_unchecked(..degree)
538        }
539    }
540
541    /// Get neighbors of a vertex (unchecked version)
542    ///
543    /// SAFETY: Caller must ensure v < MAX_SHARD_VERTICES and vertex is active
544    #[inline(always)]
545    pub unsafe fn neighbors_unchecked(&self, v: TileVertexId) -> &[AdjEntry] {
546        unsafe {
547            let entry = self.vertices.get_unchecked(v as usize);
548            let degree = entry.degree as usize;
549            self.adjacency
550                .get_unchecked(v as usize)
551                .get_unchecked(..degree)
552        }
553    }
554
555    /// Check if graph is connected (cached, call recompute_components first)
556    #[inline]
557    pub fn is_connected(&self) -> bool {
558        self.status & Self::STATUS_CONNECTED != 0
559    }
560
561    /// Compute connected components using union-find
562    ///
563    /// OPTIMIZATION: Uses iterative path compression (no recursion),
564    /// unsafe unchecked access, and processes only active edges.
565    pub fn recompute_components(&mut self) -> u16 {
566        // Simple union-find with path compression
567        let mut parent = [0u16; MAX_SHARD_VERTICES];
568        let mut rank = [0u8; MAX_SHARD_VERTICES];
569
570        // Initialize parent array
571        // OPTIMIZATION: Unrolled initialization
572        for i in 0..MAX_SHARD_VERTICES {
573            parent[i] = i as u16;
574        }
575
576        // Find with iterative path compression (no recursion overhead)
577        // OPTIMIZATION: Iterative instead of recursive, unsafe unchecked access
578        #[inline(always)]
579        fn find(parent: &mut [u16; MAX_SHARD_VERTICES], mut x: u16) -> u16 {
580            // Find root
581            let mut root = x;
582            // SAFETY: x < MAX_SHARD_VERTICES by construction
583            while unsafe { *parent.get_unchecked(root as usize) } != root {
584                root = unsafe { *parent.get_unchecked(root as usize) };
585            }
586            // Path compression
587            while x != root {
588                let next = unsafe { *parent.get_unchecked(x as usize) };
589                unsafe { *parent.get_unchecked_mut(x as usize) = root };
590                x = next;
591            }
592            root
593        }
594
595        // Union by rank
596        // OPTIMIZATION: Inlined, uses unsafe unchecked access
597        #[inline(always)]
598        fn union(
599            parent: &mut [u16; MAX_SHARD_VERTICES],
600            rank: &mut [u8; MAX_SHARD_VERTICES],
601            x: u16,
602            y: u16,
603        ) {
604            let px = find(parent, x);
605            let py = find(parent, y);
606            if px == py {
607                return;
608            }
609            // SAFETY: px, py < MAX_SHARD_VERTICES by construction
610            unsafe {
611                let rpx = *rank.get_unchecked(px as usize);
612                let rpy = *rank.get_unchecked(py as usize);
613                if rpx < rpy {
614                    *parent.get_unchecked_mut(px as usize) = py;
615                } else if rpx > rpy {
616                    *parent.get_unchecked_mut(py as usize) = px;
617                } else {
618                    *parent.get_unchecked_mut(py as usize) = px;
619                    *rank.get_unchecked_mut(px as usize) = rpx + 1;
620                }
621            }
622        }
623
624        // Process edges - only iterate up to num_edges for early termination
625        // OPTIMIZATION: Use pointer iteration for better codegen
626        for edge in self.edges.iter() {
627            if edge.is_active() {
628                union(&mut parent, &mut rank, edge.source, edge.target);
629            }
630        }
631
632        // Count components and assign component IDs
633        let mut component_count = 0u16;
634        let mut component_map = [0xFFFFu16; MAX_SHARD_VERTICES];
635
636        for i in 0..MAX_SHARD_VERTICES {
637            // SAFETY: i < MAX_SHARD_VERTICES
638            let vertex = unsafe { self.vertices.get_unchecked_mut(i) };
639            if vertex.is_active() {
640                let root = find(&mut parent, i as u16);
641                // SAFETY: root < MAX_SHARD_VERTICES
642                let mapped = unsafe { *component_map.get_unchecked(root as usize) };
643                if mapped == 0xFFFF {
644                    unsafe { *component_map.get_unchecked_mut(root as usize) = component_count };
645                    vertex.component = component_count;
646                    component_count += 1;
647                } else {
648                    vertex.component = mapped;
649                }
650            }
651        }
652
653        self.num_components = component_count;
654        if component_count <= 1 && self.num_vertices > 0 {
655            self.status |= Self::STATUS_CONNECTED;
656        } else {
657            self.status &= !Self::STATUS_CONNECTED;
658        }
659        self.status &= !Self::STATUS_DIRTY;
660
661        component_count
662    }
663
664    /// Allocate an edge slot
665    fn allocate_edge(&mut self) -> Option<TileEdgeId> {
666        // First, try free list
667        if self.free_edge_head != 0xFFFF {
668            let edge_id = self.free_edge_head;
669            // Read next from free list (stored in source field of inactive edge)
670            self.free_edge_head = self.edges[edge_id as usize].source;
671            return Some(edge_id);
672        }
673
674        // Otherwise, find first inactive edge
675        for i in 0..MAX_SHARD_EDGES {
676            if !self.edges[i].is_active() {
677                return Some(i as TileEdgeId);
678            }
679        }
680
681        None // No space
682    }
683
684    /// Return edge to free list
685    fn free_edge(&mut self, edge_id: TileEdgeId) {
686        // Use source field to store next pointer
687        self.edges[edge_id as usize].source = self.free_edge_head;
688        self.free_edge_head = edge_id;
689    }
690
691    /// Remove from adjacency list using swap-remove
692    fn remove_from_adjacency(
693        &mut self,
694        v: TileVertexId,
695        neighbor: TileVertexId,
696        edge_id: TileEdgeId,
697    ) {
698        if v as usize >= MAX_SHARD_VERTICES {
699            return;
700        }
701        let degree = self.vertices[v as usize].degree as usize;
702
703        for i in 0..degree {
704            if self.adjacency[v as usize][i].neighbor == neighbor
705                && self.adjacency[v as usize][i].edge_id == edge_id
706            {
707                // Swap with last
708                if i < degree - 1 {
709                    self.adjacency[v as usize][i] = self.adjacency[v as usize][degree - 1];
710                }
711                self.vertices[v as usize].degree -= 1;
712                return;
713            }
714        }
715    }
716
717    /// Get memory size of the graph structure
718    pub const fn memory_size() -> usize {
719        size_of::<Self>()
720    }
721
722    // ========================================================================
723    // CACHE-FRIENDLY OPTIMIZATIONS
724    // ========================================================================
725
726    /// Iterate over active vertices with cache-prefetching
727    ///
728    /// OPTIMIZATION: Uses software prefetching hints to reduce cache misses
729    /// when iterating over vertices sequentially.
730    ///
731    /// # Arguments
732    /// * `f` - Callback function receiving (vertex_id, degree, component)
733    #[inline]
734    pub fn for_each_active_vertex<F>(&self, mut f: F)
735    where
736        F: FnMut(TileVertexId, u8, u16),
737    {
738        // Process vertices in cache-line-sized chunks
739        const CHUNK_SIZE: usize = 8; // 8 * 8 bytes = 64 bytes = 1 cache line
740
741        for chunk_start in (0..MAX_SHARD_VERTICES).step_by(CHUNK_SIZE) {
742            // Process current chunk
743            let chunk_end = (chunk_start + CHUNK_SIZE).min(MAX_SHARD_VERTICES);
744
745            for i in chunk_start..chunk_end {
746                // SAFETY: i < MAX_SHARD_VERTICES by loop bounds
747                let entry = unsafe { self.vertices.get_unchecked(i) };
748                if entry.is_active() {
749                    f(i as TileVertexId, entry.degree, entry.component);
750                }
751            }
752        }
753    }
754
755    /// Iterate over active edges with cache-prefetching
756    ///
757    /// OPTIMIZATION: Processes edges in cache-line order for better locality.
758    ///
759    /// # Arguments
760    /// * `f` - Callback receiving (edge_id, source, target, weight)
761    #[inline]
762    pub fn for_each_active_edge<F>(&self, mut f: F)
763    where
764        F: FnMut(TileEdgeId, TileVertexId, TileVertexId, FixedWeight),
765    {
766        // Process edges in cache-line-sized chunks (8 edges = 64 bytes)
767        const CHUNK_SIZE: usize = 8;
768
769        for chunk_start in (0..MAX_SHARD_EDGES).step_by(CHUNK_SIZE) {
770            let chunk_end = (chunk_start + CHUNK_SIZE).min(MAX_SHARD_EDGES);
771
772            for i in chunk_start..chunk_end {
773                let edge = &self.edges[i];
774                if edge.is_active() {
775                    f(i as TileEdgeId, edge.source, edge.target, edge.weight);
776                }
777            }
778        }
779    }
780
781    /// Batch add multiple edges for improved throughput
782    ///
783    /// OPTIMIZATION: Reduces per-edge overhead by batching operations:
784    /// - Single dirty flag update
785    /// - Deferred component recomputation
786    /// - Better cache utilization
787    ///
788    /// # Arguments
789    /// * `edges` - Slice of (source, target, weight) tuples
790    ///
791    /// # Returns
792    /// Number of successfully added edges
793    #[inline]
794    pub fn add_edges_batch(
795        &mut self,
796        edges: &[(TileVertexId, TileVertexId, FixedWeight)],
797    ) -> usize {
798        let mut added = 0usize;
799
800        for &(source, target, weight) in edges {
801            if self.add_edge(source, target, weight).is_some() {
802                added += 1;
803            }
804        }
805
806        // Single generation increment for batch
807        if added > 0 {
808            self.generation = self.generation.wrapping_add(1);
809        }
810
811        added
812    }
813
814    /// Get edge weights as a contiguous slice for SIMD processing
815    ///
816    /// OPTIMIZATION: Returns a view of edge weights suitable for
817    /// SIMD operations (e.g., computing total weight, min/max).
818    ///
819    /// # Returns
820    /// Iterator of weights from active edges
821    #[inline]
822    pub fn active_edge_weights(&self) -> impl Iterator<Item = FixedWeight> + '_ {
823        self.edges
824            .iter()
825            .filter(|e| e.is_active())
826            .map(|e| e.weight)
827    }
828
829    /// Compute total edge weight using SIMD-friendly accumulation
830    ///
831    /// OPTIMIZATION: Uses parallel lane accumulation for better vectorization.
832    #[inline]
833    pub fn total_weight_simd(&self) -> u64 {
834        let mut lanes = [0u64; 4];
835
836        for (i, edge) in self.edges.iter().enumerate() {
837            if edge.is_active() {
838                lanes[i % 4] += edge.weight as u64;
839            }
840        }
841
842        lanes[0] + lanes[1] + lanes[2] + lanes[3]
843    }
844
845    /// Find minimum degree vertex efficiently
846    ///
847    /// OPTIMIZATION: Uses branch prediction hints and early exit
848    /// for finding cut boundary candidates.
849    ///
850    /// # Returns
851    /// (vertex_id, degree) of minimum degree active vertex, or None
852    #[inline]
853    pub fn min_degree_vertex(&self) -> Option<(TileVertexId, u8)> {
854        let mut min_v: Option<TileVertexId> = None;
855        let mut min_deg = u8::MAX;
856
857        for i in 0..MAX_SHARD_VERTICES {
858            let entry = &self.vertices[i];
859            // Likely hint: most vertices are inactive in sparse graphs
860            if entry.is_active() && entry.degree > 0 && entry.degree < min_deg {
861                min_deg = entry.degree;
862                min_v = Some(i as TileVertexId);
863
864                // Early exit: can't do better than degree 1
865                if min_deg == 1 {
866                    break;
867                }
868            }
869        }
870
871        min_v.map(|v| (v, min_deg))
872    }
873}
874
875// Compile-time size assertions
876const _: () = assert!(size_of::<ShardEdge>() == 8, "ShardEdge must be 8 bytes");
877const _: () = assert!(size_of::<VertexEntry>() == 8, "VertexEntry must be 8 bytes");
878const _: () = assert!(size_of::<AdjEntry>() == 4, "AdjEntry must be 4 bytes");
879// Note: CompactGraph is ~42KB which fits in our 64KB tile budget
880
881#[cfg(test)]
882mod tests {
883    use super::*;
884
885    #[test]
886    fn test_new_graph() {
887        let g = CompactGraph::new();
888        assert_eq!(g.num_vertices, 0);
889        assert_eq!(g.num_edges, 0);
890    }
891
892    #[test]
893    fn test_add_vertex() {
894        let mut g = CompactGraph::new();
895        assert!(g.add_vertex(0));
896        assert!(g.add_vertex(1));
897        assert!(!g.add_vertex(0)); // Already exists
898        assert_eq!(g.num_vertices, 2);
899    }
900
901    #[test]
902    fn test_add_edge() {
903        let mut g = CompactGraph::new();
904        let edge_id = g.add_edge(0, 1, 100);
905        assert!(edge_id.is_some());
906        assert_eq!(g.num_edges, 1);
907        assert_eq!(g.num_vertices, 2);
908        assert_eq!(g.degree(0), 1);
909        assert_eq!(g.degree(1), 1);
910    }
911
912    #[test]
913    fn test_find_edge() {
914        let mut g = CompactGraph::new();
915        g.add_edge(0, 1, 100);
916        assert!(g.find_edge(0, 1).is_some());
917        assert!(g.find_edge(1, 0).is_some());
918        assert!(g.find_edge(0, 2).is_none());
919    }
920
921    #[test]
922    fn test_remove_edge() {
923        let mut g = CompactGraph::new();
924        g.add_edge(0, 1, 100);
925        assert!(g.remove_edge(0, 1));
926        assert_eq!(g.num_edges, 0);
927        assert_eq!(g.degree(0), 0);
928        assert_eq!(g.degree(1), 0);
929    }
930
931    #[test]
932    fn test_update_weight() {
933        let mut g = CompactGraph::new();
934        g.add_edge(0, 1, 100);
935        assert!(g.update_weight(0, 1, 200));
936        assert_eq!(g.edge_weight(0, 1), Some(200));
937    }
938
939    #[test]
940    fn test_neighbors() {
941        let mut g = CompactGraph::new();
942        g.add_edge(0, 1, 100);
943        g.add_edge(0, 2, 200);
944        g.add_edge(0, 3, 300);
945
946        let neighbors = g.neighbors(0);
947        assert_eq!(neighbors.len(), 3);
948    }
949
950    #[test]
951    fn test_connected_components() {
952        let mut g = CompactGraph::new();
953        // Component 1: 0-1-2
954        g.add_edge(0, 1, 100);
955        g.add_edge(1, 2, 100);
956        // Component 2: 3-4
957        g.add_edge(3, 4, 100);
958
959        let count = g.recompute_components();
960        assert_eq!(count, 2);
961        assert!(!g.is_connected());
962    }
963
964    #[test]
965    fn test_connected_graph() {
966        let mut g = CompactGraph::new();
967        g.add_edge(0, 1, 100);
968        g.add_edge(1, 2, 100);
969        g.add_edge(2, 0, 100);
970
971        let count = g.recompute_components();
972        assert_eq!(count, 1);
973        assert!(g.is_connected());
974    }
975
976    #[test]
977    fn test_memory_size() {
978        // Verify our memory budget
979        let size = CompactGraph::memory_size();
980        assert!(size <= 65536, "CompactGraph exceeds 64KB: {} bytes", size);
981    }
982}