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 { neighbor: 0, edge_id: 0 }; MAX_DEGREE]; MAX_SHARD_VERTICES],
272        }
273    }
274
275    /// Clear the graph
276    pub fn clear(&mut self) {
277        for v in self.vertices.iter_mut() {
278            *v = VertexEntry::new();
279            v.flags = 0; // Mark as inactive
280        }
281        for e in self.edges.iter_mut() {
282            e.flags = 0;
283        }
284        self.num_vertices = 0;
285        self.num_edges = 0;
286        self.free_edge_head = 0xFFFF;
287        self.generation = self.generation.wrapping_add(1);
288        self.num_components = 0;
289        self.status = Self::STATUS_VALID | Self::STATUS_DIRTY;
290    }
291
292    /// Add or activate a vertex
293    pub fn add_vertex(&mut self, v: TileVertexId) -> bool {
294        if v as usize >= MAX_SHARD_VERTICES {
295            return false;
296        }
297
298        let entry = &mut self.vertices[v as usize];
299        if entry.is_active() {
300            return false; // Already active
301        }
302
303        entry.flags = VertexEntry::FLAG_ACTIVE;
304        entry.degree = 0;
305        entry.component = 0;
306        entry.first_edge_idx = 0xFFFF;
307        self.num_vertices += 1;
308        self.status |= Self::STATUS_DIRTY;
309        true
310    }
311
312    /// Remove a vertex (marks as inactive)
313    pub fn remove_vertex(&mut self, v: TileVertexId) -> bool {
314        if v as usize >= MAX_SHARD_VERTICES {
315            return false;
316        }
317
318        let entry = &mut self.vertices[v as usize];
319        if !entry.is_active() {
320            return false;
321        }
322
323        // Deactivate all incident edges
324        for i in 0..entry.degree as usize {
325            let adj = &self.adjacency[v as usize][i];
326            if adj.edge_id < MAX_SHARD_EDGES as u16 {
327                self.edges[adj.edge_id as usize].deactivate();
328                self.num_edges = self.num_edges.saturating_sub(1);
329            }
330        }
331
332        entry.flags = 0;
333        entry.degree = 0;
334        self.num_vertices = self.num_vertices.saturating_sub(1);
335        self.status |= Self::STATUS_DIRTY;
336        self.generation = self.generation.wrapping_add(1);
337        true
338    }
339
340    /// Add an edge between two vertices
341    pub fn add_edge(
342        &mut self,
343        source: TileVertexId,
344        target: TileVertexId,
345        weight: FixedWeight,
346    ) -> Option<TileEdgeId> {
347        // Validate vertices
348        if source as usize >= MAX_SHARD_VERTICES || target as usize >= MAX_SHARD_VERTICES {
349            return None;
350        }
351        if source == target {
352            return None; // No self-loops
353        }
354
355        // Ensure vertices are active
356        if !self.vertices[source as usize].is_active() {
357            self.add_vertex(source);
358        }
359        if !self.vertices[target as usize].is_active() {
360            self.add_vertex(target);
361        }
362
363        // Check degree limits
364        let src_entry = &self.vertices[source as usize];
365        let tgt_entry = &self.vertices[target as usize];
366        if src_entry.degree as usize >= MAX_DEGREE || tgt_entry.degree as usize >= MAX_DEGREE {
367            return None;
368        }
369
370        // Allocate edge slot
371        let edge_id = self.allocate_edge()?;
372
373        // Create edge
374        self.edges[edge_id as usize] = ShardEdge::new(source, target, weight);
375
376        // Update adjacency lists
377        let src_deg = self.vertices[source as usize].degree as usize;
378        self.adjacency[source as usize][src_deg] = AdjEntry {
379            neighbor: target,
380            edge_id,
381        };
382        self.vertices[source as usize].degree += 1;
383
384        let tgt_deg = self.vertices[target as usize].degree as usize;
385        self.adjacency[target as usize][tgt_deg] = AdjEntry {
386            neighbor: source,
387            edge_id,
388        };
389        self.vertices[target as usize].degree += 1;
390
391        self.num_edges += 1;
392        self.status |= Self::STATUS_DIRTY;
393        self.generation = self.generation.wrapping_add(1);
394
395        Some(edge_id)
396    }
397
398    /// Remove an edge
399    pub fn remove_edge(&mut self, source: TileVertexId, target: TileVertexId) -> bool {
400        // Find edge in source's adjacency
401        let edge_id = self.find_edge(source, target);
402        if edge_id.is_none() {
403            return false;
404        }
405        let edge_id = edge_id.unwrap();
406
407        // Deactivate edge
408        self.edges[edge_id as usize].deactivate();
409
410        // Remove from adjacency lists (swap-remove pattern)
411        self.remove_from_adjacency(source, target, edge_id);
412        self.remove_from_adjacency(target, source, edge_id);
413
414        // Add to free list
415        self.free_edge(edge_id);
416
417        self.num_edges = self.num_edges.saturating_sub(1);
418        self.status |= Self::STATUS_DIRTY;
419        self.generation = self.generation.wrapping_add(1);
420        true
421    }
422
423    /// Update edge weight
424    pub fn update_weight(
425        &mut self,
426        source: TileVertexId,
427        target: TileVertexId,
428        new_weight: FixedWeight,
429    ) -> bool {
430        if let Some(edge_id) = self.find_edge(source, target) {
431            self.edges[edge_id as usize].weight = new_weight;
432            self.status |= Self::STATUS_DIRTY;
433            true
434        } else {
435            false
436        }
437    }
438
439    /// Find edge between two vertices
440    ///
441    /// OPTIMIZATION: Uses unsafe unchecked access after bounds validation.
442    /// The adjacency scan is a hot path in graph algorithms.
443    #[inline]
444    pub fn find_edge(&self, source: TileVertexId, target: TileVertexId) -> Option<TileEdgeId> {
445        if source as usize >= MAX_SHARD_VERTICES {
446            return None;
447        }
448
449        // SAFETY: source bounds checked above
450        let entry = unsafe { self.vertices.get_unchecked(source as usize) };
451        if !entry.is_active() {
452            return None;
453        }
454
455        let degree = entry.degree as usize;
456        // SAFETY: source bounds checked, degree <= MAX_DEGREE by invariant
457        let adj_list = unsafe { self.adjacency.get_unchecked(source as usize) };
458
459        for i in 0..degree {
460            // SAFETY: i < degree <= MAX_DEGREE
461            let adj = unsafe { adj_list.get_unchecked(i) };
462            if adj.neighbor == target {
463                return Some(adj.edge_id);
464            }
465        }
466        None
467    }
468
469    /// Find edge between two vertices (unchecked version)
470    ///
471    /// SAFETY: Caller must ensure source < MAX_SHARD_VERTICES and vertex is active
472    #[inline(always)]
473    pub unsafe fn find_edge_unchecked(&self, source: TileVertexId, target: TileVertexId) -> Option<TileEdgeId> {
474        unsafe {
475            let entry = self.vertices.get_unchecked(source as usize);
476            let degree = entry.degree as usize;
477            let adj_list = self.adjacency.get_unchecked(source as usize);
478
479            for i in 0..degree {
480                let adj = adj_list.get_unchecked(i);
481                if adj.neighbor == target {
482                    return Some(adj.edge_id);
483                }
484            }
485            None
486        }
487    }
488
489    /// Get edge weight
490    pub fn edge_weight(&self, source: TileVertexId, target: TileVertexId) -> Option<FixedWeight> {
491        self.find_edge(source, target)
492            .map(|eid| self.edges[eid as usize].weight)
493    }
494
495    /// Get vertex degree
496    ///
497    /// OPTIMIZATION: Uses unsafe unchecked access after bounds check
498    #[inline(always)]
499    pub fn degree(&self, v: TileVertexId) -> u8 {
500        if v as usize >= MAX_SHARD_VERTICES {
501            return 0;
502        }
503        // SAFETY: bounds checked above
504        let entry = unsafe { self.vertices.get_unchecked(v as usize) };
505        if entry.is_active() {
506            entry.degree
507        } else {
508            0
509        }
510    }
511
512    /// Get neighbors of a vertex
513    ///
514    /// OPTIMIZATION: Uses unsafe unchecked slice creation after bounds check
515    #[inline]
516    pub fn neighbors(&self, v: TileVertexId) -> &[AdjEntry] {
517        if v as usize >= MAX_SHARD_VERTICES {
518            return &[];
519        }
520        // SAFETY: bounds checked above
521        let entry = unsafe { self.vertices.get_unchecked(v as usize) };
522        if !entry.is_active() {
523            return &[];
524        }
525        let degree = entry.degree as usize;
526        // SAFETY: bounds checked, degree <= MAX_DEGREE by invariant
527        unsafe {
528            self.adjacency
529                .get_unchecked(v as usize)
530                .get_unchecked(..degree)
531        }
532    }
533
534    /// Get neighbors of a vertex (unchecked version)
535    ///
536    /// SAFETY: Caller must ensure v < MAX_SHARD_VERTICES and vertex is active
537    #[inline(always)]
538    pub unsafe fn neighbors_unchecked(&self, v: TileVertexId) -> &[AdjEntry] {
539        unsafe {
540            let entry = self.vertices.get_unchecked(v as usize);
541            let degree = entry.degree as usize;
542            self.adjacency
543                .get_unchecked(v as usize)
544                .get_unchecked(..degree)
545        }
546    }
547
548    /// Check if graph is connected (cached, call recompute_components first)
549    #[inline]
550    pub fn is_connected(&self) -> bool {
551        self.status & Self::STATUS_CONNECTED != 0
552    }
553
554    /// Compute connected components using union-find
555    ///
556    /// OPTIMIZATION: Uses iterative path compression (no recursion),
557    /// unsafe unchecked access, and processes only active edges.
558    pub fn recompute_components(&mut self) -> u16 {
559        // Simple union-find with path compression
560        let mut parent = [0u16; MAX_SHARD_VERTICES];
561        let mut rank = [0u8; MAX_SHARD_VERTICES];
562
563        // Initialize parent array
564        // OPTIMIZATION: Unrolled initialization
565        for i in 0..MAX_SHARD_VERTICES {
566            parent[i] = i as u16;
567        }
568
569        // Find with iterative path compression (no recursion overhead)
570        // OPTIMIZATION: Iterative instead of recursive, unsafe unchecked access
571        #[inline(always)]
572        fn find(parent: &mut [u16; MAX_SHARD_VERTICES], mut x: u16) -> u16 {
573            // Find root
574            let mut root = x;
575            // SAFETY: x < MAX_SHARD_VERTICES by construction
576            while unsafe { *parent.get_unchecked(root as usize) } != root {
577                root = unsafe { *parent.get_unchecked(root as usize) };
578            }
579            // Path compression
580            while x != root {
581                let next = unsafe { *parent.get_unchecked(x as usize) };
582                unsafe { *parent.get_unchecked_mut(x as usize) = root };
583                x = next;
584            }
585            root
586        }
587
588        // Union by rank
589        // OPTIMIZATION: Inlined, uses unsafe unchecked access
590        #[inline(always)]
591        fn union(
592            parent: &mut [u16; MAX_SHARD_VERTICES],
593            rank: &mut [u8; MAX_SHARD_VERTICES],
594            x: u16,
595            y: u16,
596        ) {
597            let px = find(parent, x);
598            let py = find(parent, y);
599            if px == py {
600                return;
601            }
602            // SAFETY: px, py < MAX_SHARD_VERTICES by construction
603            unsafe {
604                let rpx = *rank.get_unchecked(px as usize);
605                let rpy = *rank.get_unchecked(py as usize);
606                if rpx < rpy {
607                    *parent.get_unchecked_mut(px as usize) = py;
608                } else if rpx > rpy {
609                    *parent.get_unchecked_mut(py as usize) = px;
610                } else {
611                    *parent.get_unchecked_mut(py as usize) = px;
612                    *rank.get_unchecked_mut(px as usize) = rpx + 1;
613                }
614            }
615        }
616
617        // Process edges - only iterate up to num_edges for early termination
618        // OPTIMIZATION: Use pointer iteration for better codegen
619        for edge in self.edges.iter() {
620            if edge.is_active() {
621                union(&mut parent, &mut rank, edge.source, edge.target);
622            }
623        }
624
625        // Count components and assign component IDs
626        let mut component_count = 0u16;
627        let mut component_map = [0xFFFFu16; MAX_SHARD_VERTICES];
628
629        for i in 0..MAX_SHARD_VERTICES {
630            // SAFETY: i < MAX_SHARD_VERTICES
631            let vertex = unsafe { self.vertices.get_unchecked_mut(i) };
632            if vertex.is_active() {
633                let root = find(&mut parent, i as u16);
634                // SAFETY: root < MAX_SHARD_VERTICES
635                let mapped = unsafe { *component_map.get_unchecked(root as usize) };
636                if mapped == 0xFFFF {
637                    unsafe { *component_map.get_unchecked_mut(root as usize) = component_count };
638                    vertex.component = component_count;
639                    component_count += 1;
640                } else {
641                    vertex.component = mapped;
642                }
643            }
644        }
645
646        self.num_components = component_count;
647        if component_count <= 1 && self.num_vertices > 0 {
648            self.status |= Self::STATUS_CONNECTED;
649        } else {
650            self.status &= !Self::STATUS_CONNECTED;
651        }
652        self.status &= !Self::STATUS_DIRTY;
653
654        component_count
655    }
656
657    /// Allocate an edge slot
658    fn allocate_edge(&mut self) -> Option<TileEdgeId> {
659        // First, try free list
660        if self.free_edge_head != 0xFFFF {
661            let edge_id = self.free_edge_head;
662            // Read next from free list (stored in source field of inactive edge)
663            self.free_edge_head = self.edges[edge_id as usize].source;
664            return Some(edge_id);
665        }
666
667        // Otherwise, find first inactive edge
668        for i in 0..MAX_SHARD_EDGES {
669            if !self.edges[i].is_active() {
670                return Some(i as TileEdgeId);
671            }
672        }
673
674        None // No space
675    }
676
677    /// Return edge to free list
678    fn free_edge(&mut self, edge_id: TileEdgeId) {
679        // Use source field to store next pointer
680        self.edges[edge_id as usize].source = self.free_edge_head;
681        self.free_edge_head = edge_id;
682    }
683
684    /// Remove from adjacency list using swap-remove
685    fn remove_from_adjacency(&mut self, v: TileVertexId, neighbor: TileVertexId, edge_id: TileEdgeId) {
686        if v as usize >= MAX_SHARD_VERTICES {
687            return;
688        }
689        let degree = self.vertices[v as usize].degree as usize;
690
691        for i in 0..degree {
692            if self.adjacency[v as usize][i].neighbor == neighbor
693                && self.adjacency[v as usize][i].edge_id == edge_id
694            {
695                // Swap with last
696                if i < degree - 1 {
697                    self.adjacency[v as usize][i] = self.adjacency[v as usize][degree - 1];
698                }
699                self.vertices[v as usize].degree -= 1;
700                return;
701            }
702        }
703    }
704
705    /// Get memory size of the graph structure
706    pub const fn memory_size() -> usize {
707        size_of::<Self>()
708    }
709
710    // ========================================================================
711    // CACHE-FRIENDLY OPTIMIZATIONS
712    // ========================================================================
713
714    /// Iterate over active vertices with cache-prefetching
715    ///
716    /// OPTIMIZATION: Uses software prefetching hints to reduce cache misses
717    /// when iterating over vertices sequentially.
718    ///
719    /// # Arguments
720    /// * `f` - Callback function receiving (vertex_id, degree, component)
721    #[inline]
722    pub fn for_each_active_vertex<F>(&self, mut f: F)
723    where
724        F: FnMut(TileVertexId, u8, u16),
725    {
726        // Process vertices in cache-line-sized chunks
727        const CHUNK_SIZE: usize = 8; // 8 * 8 bytes = 64 bytes = 1 cache line
728
729        for chunk_start in (0..MAX_SHARD_VERTICES).step_by(CHUNK_SIZE) {
730            // Process current chunk
731            let chunk_end = (chunk_start + CHUNK_SIZE).min(MAX_SHARD_VERTICES);
732
733            for i in chunk_start..chunk_end {
734                // SAFETY: i < MAX_SHARD_VERTICES by loop bounds
735                let entry = unsafe { self.vertices.get_unchecked(i) };
736                if entry.is_active() {
737                    f(i as TileVertexId, entry.degree, entry.component);
738                }
739            }
740        }
741    }
742
743    /// Iterate over active edges with cache-prefetching
744    ///
745    /// OPTIMIZATION: Processes edges in cache-line order for better locality.
746    ///
747    /// # Arguments
748    /// * `f` - Callback receiving (edge_id, source, target, weight)
749    #[inline]
750    pub fn for_each_active_edge<F>(&self, mut f: F)
751    where
752        F: FnMut(TileEdgeId, TileVertexId, TileVertexId, FixedWeight),
753    {
754        // Process edges in cache-line-sized chunks (8 edges = 64 bytes)
755        const CHUNK_SIZE: usize = 8;
756
757        for chunk_start in (0..MAX_SHARD_EDGES).step_by(CHUNK_SIZE) {
758            let chunk_end = (chunk_start + CHUNK_SIZE).min(MAX_SHARD_EDGES);
759
760            for i in chunk_start..chunk_end {
761                let edge = &self.edges[i];
762                if edge.is_active() {
763                    f(i as TileEdgeId, edge.source, edge.target, edge.weight);
764                }
765            }
766        }
767    }
768
769    /// Batch add multiple edges for improved throughput
770    ///
771    /// OPTIMIZATION: Reduces per-edge overhead by batching operations:
772    /// - Single dirty flag update
773    /// - Deferred component recomputation
774    /// - Better cache utilization
775    ///
776    /// # Arguments
777    /// * `edges` - Slice of (source, target, weight) tuples
778    ///
779    /// # Returns
780    /// Number of successfully added edges
781    #[inline]
782    pub fn add_edges_batch(
783        &mut self,
784        edges: &[(TileVertexId, TileVertexId, FixedWeight)],
785    ) -> usize {
786        let mut added = 0usize;
787
788        for &(source, target, weight) in edges {
789            if self.add_edge(source, target, weight).is_some() {
790                added += 1;
791            }
792        }
793
794        // Single generation increment for batch
795        if added > 0 {
796            self.generation = self.generation.wrapping_add(1);
797        }
798
799        added
800    }
801
802    /// Get edge weights as a contiguous slice for SIMD processing
803    ///
804    /// OPTIMIZATION: Returns a view of edge weights suitable for
805    /// SIMD operations (e.g., computing total weight, min/max).
806    ///
807    /// # Returns
808    /// Iterator of weights from active edges
809    #[inline]
810    pub fn active_edge_weights(&self) -> impl Iterator<Item = FixedWeight> + '_ {
811        self.edges.iter().filter(|e| e.is_active()).map(|e| e.weight)
812    }
813
814    /// Compute total edge weight using SIMD-friendly accumulation
815    ///
816    /// OPTIMIZATION: Uses parallel lane accumulation for better vectorization.
817    #[inline]
818    pub fn total_weight_simd(&self) -> u64 {
819        let mut lanes = [0u64; 4];
820
821        for (i, edge) in self.edges.iter().enumerate() {
822            if edge.is_active() {
823                lanes[i % 4] += edge.weight as u64;
824            }
825        }
826
827        lanes[0] + lanes[1] + lanes[2] + lanes[3]
828    }
829
830    /// Find minimum degree vertex efficiently
831    ///
832    /// OPTIMIZATION: Uses branch prediction hints and early exit
833    /// for finding cut boundary candidates.
834    ///
835    /// # Returns
836    /// (vertex_id, degree) of minimum degree active vertex, or None
837    #[inline]
838    pub fn min_degree_vertex(&self) -> Option<(TileVertexId, u8)> {
839        let mut min_v: Option<TileVertexId> = None;
840        let mut min_deg = u8::MAX;
841
842        for i in 0..MAX_SHARD_VERTICES {
843            let entry = &self.vertices[i];
844            // Likely hint: most vertices are inactive in sparse graphs
845            if entry.is_active() && entry.degree > 0 && entry.degree < min_deg {
846                min_deg = entry.degree;
847                min_v = Some(i as TileVertexId);
848
849                // Early exit: can't do better than degree 1
850                if min_deg == 1 {
851                    break;
852                }
853            }
854        }
855
856        min_v.map(|v| (v, min_deg))
857    }
858}
859
860// Compile-time size assertions
861const _: () = assert!(size_of::<ShardEdge>() == 8, "ShardEdge must be 8 bytes");
862const _: () = assert!(size_of::<VertexEntry>() == 8, "VertexEntry must be 8 bytes");
863const _: () = assert!(size_of::<AdjEntry>() == 4, "AdjEntry must be 4 bytes");
864// Note: CompactGraph is ~42KB which fits in our 64KB tile budget
865
866#[cfg(test)]
867mod tests {
868    use super::*;
869
870    #[test]
871    fn test_new_graph() {
872        let g = CompactGraph::new();
873        assert_eq!(g.num_vertices, 0);
874        assert_eq!(g.num_edges, 0);
875    }
876
877    #[test]
878    fn test_add_vertex() {
879        let mut g = CompactGraph::new();
880        assert!(g.add_vertex(0));
881        assert!(g.add_vertex(1));
882        assert!(!g.add_vertex(0)); // Already exists
883        assert_eq!(g.num_vertices, 2);
884    }
885
886    #[test]
887    fn test_add_edge() {
888        let mut g = CompactGraph::new();
889        let edge_id = g.add_edge(0, 1, 100);
890        assert!(edge_id.is_some());
891        assert_eq!(g.num_edges, 1);
892        assert_eq!(g.num_vertices, 2);
893        assert_eq!(g.degree(0), 1);
894        assert_eq!(g.degree(1), 1);
895    }
896
897    #[test]
898    fn test_find_edge() {
899        let mut g = CompactGraph::new();
900        g.add_edge(0, 1, 100);
901        assert!(g.find_edge(0, 1).is_some());
902        assert!(g.find_edge(1, 0).is_some());
903        assert!(g.find_edge(0, 2).is_none());
904    }
905
906    #[test]
907    fn test_remove_edge() {
908        let mut g = CompactGraph::new();
909        g.add_edge(0, 1, 100);
910        assert!(g.remove_edge(0, 1));
911        assert_eq!(g.num_edges, 0);
912        assert_eq!(g.degree(0), 0);
913        assert_eq!(g.degree(1), 0);
914    }
915
916    #[test]
917    fn test_update_weight() {
918        let mut g = CompactGraph::new();
919        g.add_edge(0, 1, 100);
920        assert!(g.update_weight(0, 1, 200));
921        assert_eq!(g.edge_weight(0, 1), Some(200));
922    }
923
924    #[test]
925    fn test_neighbors() {
926        let mut g = CompactGraph::new();
927        g.add_edge(0, 1, 100);
928        g.add_edge(0, 2, 200);
929        g.add_edge(0, 3, 300);
930
931        let neighbors = g.neighbors(0);
932        assert_eq!(neighbors.len(), 3);
933    }
934
935    #[test]
936    fn test_connected_components() {
937        let mut g = CompactGraph::new();
938        // Component 1: 0-1-2
939        g.add_edge(0, 1, 100);
940        g.add_edge(1, 2, 100);
941        // Component 2: 3-4
942        g.add_edge(3, 4, 100);
943
944        let count = g.recompute_components();
945        assert_eq!(count, 2);
946        assert!(!g.is_connected());
947    }
948
949    #[test]
950    fn test_connected_graph() {
951        let mut g = CompactGraph::new();
952        g.add_edge(0, 1, 100);
953        g.add_edge(1, 2, 100);
954        g.add_edge(2, 0, 100);
955
956        let count = g.recompute_components();
957        assert_eq!(count, 1);
958        assert!(g.is_connected());
959    }
960
961    #[test]
962    fn test_memory_size() {
963        // Verify our memory budget
964        let size = CompactGraph::memory_size();
965        assert!(size <= 65536, "CompactGraph exceeds 64KB: {} bytes", size);
966    }
967}