cognitum-gate-kernel 0.1.1

No-std WASM kernel for 256-tile coherence gate fabric
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
//! Canonical witness fragments using pseudo-deterministic min-cut.
//!
//! Produces reproducible, hash-stable witness fragments by computing
//! a canonical min-cut partition via lexicographic tie-breaking.
//!
//! All structures are `#[repr(C)]` aligned, use fixed-size arrays, and
//! operate entirely on the stack (no heap allocation). This module is
//! designed for no_std WASM tiles with a ~2.1KB temporary memory footprint.

#![allow(missing_docs)]

use crate::shard::{CompactGraph, MAX_SHARD_VERTICES};
use core::mem::size_of;

// ============================================================================
// Fixed-point weight for deterministic comparison
// ============================================================================

/// Fixed-point weight for deterministic, total-order comparison.
///
/// Uses 16.16 fixed-point representation (upper 16 bits integer, lower 16
/// bits fractional). This avoids floating-point non-determinism in
/// partition comparisons.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
#[repr(transparent)]
pub struct FixedPointWeight(pub u32);

impl FixedPointWeight {
    /// Zero weight constant
    pub const ZERO: Self = Self(0);

    /// One (1.0) in 16.16 fixed-point
    pub const ONE: Self = Self(65536);

    /// Maximum representable weight
    pub const MAX: Self = Self(u32::MAX);

    /// Convert from a `ShardEdge` weight (u16, 0.01 precision) to fixed-point.
    ///
    /// The shard weight is scaled up by shifting left 8 bits, mapping
    /// the 0-65535 range into the 16.16 fixed-point space.
    #[inline(always)]
    pub const fn from_u16_weight(w: u16) -> Self {
        Self((w as u32) << 8)
    }

    /// Saturating addition (clamps at `u32::MAX`)
    #[inline(always)]
    pub const fn saturating_add(self, other: Self) -> Self {
        Self(self.0.saturating_add(other.0))
    }

    /// Saturating subtraction (clamps at 0)
    #[inline(always)]
    pub const fn saturating_sub(self, other: Self) -> Self {
        Self(self.0.saturating_sub(other.0))
    }

    /// Truncate to u16 by shifting right 8 bits (inverse of `from_u16_weight`)
    #[inline(always)]
    pub const fn to_u16(self) -> u16 {
        (self.0 >> 8) as u16
    }
}

// ============================================================================
// Cactus node and arena
// ============================================================================

/// A single node in the arena-allocated cactus tree.
///
/// Represents a vertex (or contracted 2-edge-connected component) in the
/// simplified cactus structure derived from the tile's compact graph.
#[derive(Debug, Copy, Clone)]
#[repr(C)]
pub struct CactusNode {
    /// Vertex ID in the original graph
    pub id: u16,
    /// Parent index in `ArenaCactus::nodes` (0xFFFF = root / no parent)
    pub parent: u16,
    /// Degree in the cactus tree
    pub degree: u8,
    /// Flags (reserved)
    pub flags: u8,
    /// Weight of the edge connecting this node to its parent
    pub weight_to_parent: FixedPointWeight,
}

impl CactusNode {
    /// Sentinel value indicating no parent (root node)
    pub const NO_PARENT: u16 = 0xFFFF;

    /// Create an empty / default node
    #[inline(always)]
    pub const fn empty() -> Self {
        Self {
            id: 0,
            parent: Self::NO_PARENT,
            degree: 0,
            flags: 0,
            weight_to_parent: FixedPointWeight::ZERO,
        }
    }
}

// Compile-time size check: repr(C) layout is 12 bytes
// (u16 + u16 + u8 + u8 + 2-pad + u32 = 12, aligned to 4)
// 256 nodes * 12 = 3072 bytes (~3KB), fits in 14.5KB headroom.
const _: () = assert!(
    size_of::<CactusNode>() == 12,
    "CactusNode must be 12 bytes"
);

/// Arena-allocated cactus tree for a single tile (up to 256 vertices).
///
/// The cactus captures the 2-edge-connected component structure of the
/// tile's local graph. It is built entirely on the stack (~2KB) and used
/// to derive a canonical min-cut partition.
#[repr(C)]
pub struct ArenaCactus {
    /// Node storage (one per vertex in the original graph)
    pub nodes: [CactusNode; 256],
    /// Number of active nodes
    pub n_nodes: u16,
    /// Root node index
    pub root: u16,
    /// Value of the global minimum cut found
    pub min_cut_value: FixedPointWeight,
}

impl ArenaCactus {
    /// Build a cactus from the tile's `CompactGraph`.
    ///
    /// Algorithm (simplified):
    /// 1. BFS spanning tree from the lowest-ID active vertex.
    /// 2. Identify back edges and compute 2-edge-connected components
    ///    via low-link (Tarjan-style on edges).
    /// 3. Contract each 2-edge-connected component into a single cactus
    ///    node; the inter-component bridge edges become cactus edges.
    /// 4. Track the minimum-weight bridge as the global min-cut value.
    pub fn build_from_compact_graph(graph: &CompactGraph) -> Self {
        let mut cactus = ArenaCactus {
            nodes: [CactusNode::empty(); 256],
            n_nodes: 0,
            root: 0xFFFF,
            min_cut_value: FixedPointWeight::MAX,
        };

        if graph.num_vertices == 0 {
            cactus.min_cut_value = FixedPointWeight::ZERO;
            return cactus;
        }

        // ---- Phase 1: BFS spanning tree ----
        // BFS queue (fixed-size ring buffer)
        let mut queue = [0u16; 256];
        let mut q_head: usize = 0;
        let mut q_tail: usize = 0;

        // Per-vertex BFS state
        let mut visited = [false; MAX_SHARD_VERTICES];
        let mut parent = [0xFFFFu16; MAX_SHARD_VERTICES];
        let mut depth = [0u16; MAX_SHARD_VERTICES];
        // Component ID for 2-edge-connected grouping
        let mut comp_id = [0xFFFFu16; MAX_SHARD_VERTICES];

        // Find lowest-ID active vertex as root
        let mut root_v = 0xFFFFu16;
        for v in 0..MAX_SHARD_VERTICES {
            if graph.vertices[v].is_active() {
                root_v = v as u16;
                break;
            }
        }

        if root_v == 0xFFFF {
            cactus.min_cut_value = FixedPointWeight::ZERO;
            return cactus;
        }

        // BFS
        visited[root_v as usize] = true;
        parent[root_v as usize] = 0xFFFF;
        queue[q_tail] = root_v;
        q_tail += 1;

        while q_head < q_tail {
            let u = queue[q_head] as usize;
            q_head += 1;

            let neighbors = graph.neighbors(u as u16);
            for adj in neighbors {
                let w = adj.neighbor as usize;
                if !visited[w] {
                    visited[w] = true;
                    parent[w] = u as u16;
                    depth[w] = depth[u] + 1;
                    if q_tail < 256 {
                        queue[q_tail] = w as u16;
                        q_tail += 1;
                    }
                }
            }
        }

        // ---- Phase 2: Identify 2-edge-connected components ----
        // For each back edge (u,w) where w is an ancestor of u in the BFS tree,
        // all vertices on the path from u to w belong to the same 2-edge-connected
        // component. We perform path marking for each back edge.
        let mut next_comp: u16 = 0;

        // Mark tree edges as bridges initially; back edges will un-bridge them
        // We iterate edges and find back edges (both endpoints visited, not parent-child)
        for e_idx in 0..graph.edges.len() {
            let edge = &graph.edges[e_idx];
            if !edge.is_active() {
                continue;
            }
            let u = edge.source as usize;
            let w = edge.target as usize;

            if !visited[u] || !visited[w] {
                continue;
            }

            // Check if this is a back edge (non-tree edge)
            let is_tree = (parent[w] == u as u16 && depth[w] == depth[u] + 1)
                || (parent[u] == w as u16 && depth[u] == depth[w] + 1);

            if is_tree {
                continue; // Skip tree edges
            }

            // Back edge found: mark the path from u to w as same component
            // Walk u and w up to their LCA, assigning a single component ID
            let c = if comp_id[u] != 0xFFFF {
                comp_id[u]
            } else if comp_id[w] != 0xFFFF {
                comp_id[w]
            } else {
                let c = next_comp;
                next_comp = next_comp.saturating_add(1);
                c
            };

            // Walk from u towards root, marking component
            let mut a = u as u16;
            while a != 0xFFFF && comp_id[a as usize] != c {
                if comp_id[a as usize] == 0xFFFF {
                    comp_id[a as usize] = c;
                }
                a = parent[a as usize];
            }

            // Walk from w towards root, marking component
            let mut b = w as u16;
            while b != 0xFFFF && comp_id[b as usize] != c {
                if comp_id[b as usize] == 0xFFFF {
                    comp_id[b as usize] = c;
                }
                b = parent[b as usize];
            }
        }

        // Assign each unmarked visited vertex its own component
        for v in 0..MAX_SHARD_VERTICES {
            if visited[v] && comp_id[v] == 0xFFFF {
                comp_id[v] = next_comp;
                next_comp = next_comp.saturating_add(1);
            }
        }

        // ---- Phase 3: Build cactus from component structure ----
        // Each unique comp_id becomes a cactus node.
        // The representative vertex is the lowest-ID vertex in the component.
        let mut comp_repr = [0xFFFFu16; 256]; // comp_id -> representative vertex
        let mut comp_to_node = [0xFFFFu16; 256]; // comp_id -> cactus node index

        // Find representative (lowest vertex ID) for each component
        for v in 0..MAX_SHARD_VERTICES {
            if !visited[v] {
                continue;
            }
            let c = comp_id[v] as usize;
            if c < 256 && (comp_repr[c] == 0xFFFF || (v as u16) < comp_repr[c]) {
                comp_repr[c] = v as u16;
            }
        }

        // Create cactus nodes for each component
        let mut n_cactus: u16 = 0;
        for c in 0..next_comp.min(256) as usize {
            if comp_repr[c] != 0xFFFF {
                let idx = n_cactus as usize;
                if idx < 256 {
                    cactus.nodes[idx] = CactusNode {
                        id: comp_repr[c],
                        parent: CactusNode::NO_PARENT,
                        degree: 0,
                        flags: 0,
                        weight_to_parent: FixedPointWeight::ZERO,
                    };
                    comp_to_node[c] = n_cactus;
                    n_cactus += 1;
                }
            }
        }

        cactus.n_nodes = n_cactus;

        // Set root to the node containing root_v
        let root_comp = comp_id[root_v as usize] as usize;
        if root_comp < 256 {
            cactus.root = comp_to_node[root_comp];
        }

        // ---- Phase 4: Connect cactus nodes via bridge edges ----
        // A tree edge (parent[v] -> v) where comp_id[parent[v]] != comp_id[v]
        // is a bridge. It becomes a cactus edge.
        for v in 0..MAX_SHARD_VERTICES {
            if !visited[v] || parent[v] == 0xFFFF {
                continue;
            }
            let p = parent[v] as usize;
            let cv = comp_id[v] as usize;
            let cp = comp_id[p] as usize;

            if cv != cp && cv < 256 && cp < 256 {
                let node_v = comp_to_node[cv];
                let node_p = comp_to_node[cp];

                if node_v < 256 && node_p < 256
                    && cactus.nodes[node_v as usize].parent == CactusNode::NO_PARENT
                    && node_v != cactus.root
                {
                    // Compute bridge weight: sum of edge weights between the
                    // two components along this boundary
                    let bridge_weight = Self::compute_bridge_weight(graph, v as u16, parent[v]);

                    cactus.nodes[node_v as usize].parent = node_p;
                    cactus.nodes[node_v as usize].weight_to_parent = bridge_weight;
                    cactus.nodes[node_p as usize].degree += 1;
                    cactus.nodes[node_v as usize].degree += 1;

                    // Track minimum cut
                    if bridge_weight < cactus.min_cut_value {
                        cactus.min_cut_value = bridge_weight;
                    }
                }
            }
        }

        // If no bridges found, min cut is sum of all edge weights (graph is
        // 2-edge-connected) or zero if there are no edges
        if cactus.min_cut_value == FixedPointWeight::MAX {
            if graph.num_edges == 0 {
                cactus.min_cut_value = FixedPointWeight::ZERO;
            } else {
                // 2-edge-connected: min cut is at least the minimum degree
                // weight sum. Compute as total weight / 2 as rough upper bound
                // or just report the minimum vertex weighted degree.
                cactus.min_cut_value = Self::min_vertex_weight_degree(graph);
            }
        }

        cactus
    }

    /// Compute bridge weight between two vertices that are in different
    /// 2-edge-connected components.
    fn compute_bridge_weight(graph: &CompactGraph, v: u16, p: u16) -> FixedPointWeight {
        // Find the edge between v and p and return its weight
        if let Some(eid) = graph.find_edge(v, p) {
            FixedPointWeight::from_u16_weight(graph.edges[eid as usize].weight)
        } else {
            FixedPointWeight::ONE
        }
    }

    /// Compute minimum vertex weighted degree in the graph.
    fn min_vertex_weight_degree(graph: &CompactGraph) -> FixedPointWeight {
        let mut min_weight = FixedPointWeight::MAX;

        for v in 0..MAX_SHARD_VERTICES {
            if !graph.vertices[v].is_active() || graph.vertices[v].degree == 0 {
                continue;
            }
            let mut weight_sum = FixedPointWeight::ZERO;
            let neighbors = graph.neighbors(v as u16);
            for adj in neighbors {
                let eid = adj.edge_id as usize;
                if eid < graph.edges.len() && graph.edges[eid].is_active() {
                    weight_sum =
                        weight_sum.saturating_add(FixedPointWeight::from_u16_weight(
                            graph.edges[eid].weight,
                        ));
                }
            }
            if weight_sum < min_weight {
                min_weight = weight_sum;
            }
        }

        if min_weight == FixedPointWeight::MAX {
            FixedPointWeight::ZERO
        } else {
            min_weight
        }
    }

    /// Derive the canonical (lex-smallest) partition from this cactus.
    ///
    /// Finds the minimum-weight edge in the cactus, removes it to create
    /// two subtrees, and assigns the subtree with the lex-smallest vertex
    /// set to side A. Ties are broken by selecting the edge whose removal
    /// yields the lex-smallest side-A bitset.
    pub fn canonical_partition(&self) -> CanonicalPartition {
        let mut best = CanonicalPartition::empty();

        if self.n_nodes <= 1 {
            // Trivial: all vertices on side A
            best.cardinality_a = self.n_nodes;
            best.cut_value = FixedPointWeight::ZERO;
            best.compute_hash();
            return best;
        }

        // Find the minimum-weight cactus edge. For each non-root node whose
        // edge to its parent has weight == min_cut_value, compute the
        // resulting partition and keep the lex-smallest.
        let mut found = false;

        for i in 0..self.n_nodes as usize {
            let node = &self.nodes[i];
            if node.parent == CactusNode::NO_PARENT {
                continue; // Root has no parent edge
            }
            if node.weight_to_parent != self.min_cut_value {
                continue; // Not a minimum edge
            }

            // Removing this edge splits the cactus into:
            //   subtree rooted at node i  vs  everything else
            let mut candidate = CanonicalPartition::empty();
            candidate.cut_value = self.min_cut_value;

            // Mark the subtree rooted at node i as side B
            self.mark_subtree(i as u16, &mut candidate);

            // Count cardinalities
            candidate.recount();

            // Ensure canonical orientation: side A should have lex-smallest
            // vertex set. If side B is lex-smaller, flip.
            if !candidate.is_canonical() {
                candidate.flip();
            }

            candidate.compute_hash();

            if !found || candidate.side < best.side {
                best = candidate;
                found = true;
            }
        }

        if !found {
            best.compute_hash();
        }

        best
    }

    /// Mark all nodes in the subtree rooted at `start` to side B.
    fn mark_subtree(&self, start: u16, partition: &mut CanonicalPartition) {
        // The cactus tree has parent pointers, so we find all nodes
        // whose ancestor chain leads to `start` (before reaching the root
        // or a node not descended from `start`).
        partition.set_side(self.nodes[start as usize].id, true);

        for i in 0..self.n_nodes as usize {
            if i == start as usize {
                continue;
            }
            // Walk ancestor chain to see if this node is in start's subtree
            let mut cur = i as u16;
            let mut in_subtree = false;
            let mut steps = 0u16;
            while cur != CactusNode::NO_PARENT && steps < 256 {
                if cur == start {
                    in_subtree = true;
                    break;
                }
                cur = self.nodes[cur as usize].parent;
                steps += 1;
            }
            if in_subtree {
                partition.set_side(self.nodes[i].id, true);
            }
        }
    }

    /// Compute a 16-bit digest of the cactus structure for embedding
    /// in the witness fragment.
    pub fn digest(&self) -> u16 {
        let mut hash: u32 = 0x811c9dc5;
        for i in 0..self.n_nodes as usize {
            let node = &self.nodes[i];
            hash ^= node.id as u32;
            hash = hash.wrapping_mul(0x01000193);
            hash ^= node.parent as u32;
            hash = hash.wrapping_mul(0x01000193);
            hash ^= node.weight_to_parent.0;
            hash = hash.wrapping_mul(0x01000193);
        }
        (hash & 0xFFFF) as u16
    }
}

// ============================================================================
// Canonical partition
// ============================================================================

/// A canonical two-way partition of vertices into sides A and B.
///
/// The bitset encodes 256 vertices (1 bit each = 32 bytes). A cleared
/// bit means side A, a set bit means side B. The canonical orientation
/// guarantees that side A contains the lex-smallest vertex set.
#[derive(Debug, Copy, Clone)]
#[repr(C)]
pub struct CanonicalPartition {
    /// Bitset: 256 vertices, 1 bit each (0 = side A, 1 = side B)
    pub side: [u8; 32],
    /// Number of vertices on side A
    pub cardinality_a: u16,
    /// Number of vertices on side B
    pub cardinality_b: u16,
    /// Cut value (weight of edges crossing the partition)
    pub cut_value: FixedPointWeight,
    /// 32-bit FNV-1a hash of the `side` bitset
    pub canonical_hash: [u8; 4],
}

impl CanonicalPartition {
    /// Create an empty partition (all vertices on side A)
    #[inline]
    pub const fn empty() -> Self {
        Self {
            side: [0u8; 32],
            cardinality_a: 0,
            cardinality_b: 0,
            cut_value: FixedPointWeight::ZERO,
            canonical_hash: [0u8; 4],
        }
    }

    /// Set which side a vertex belongs to.
    ///
    /// `side_b = false` means side A, `side_b = true` means side B.
    #[inline]
    pub fn set_side(&mut self, vertex: u16, side_b: bool) {
        if vertex >= 256 {
            return;
        }
        let byte_idx = (vertex / 8) as usize;
        let bit_idx = vertex % 8;
        if side_b {
            self.side[byte_idx] |= 1 << bit_idx;
        } else {
            self.side[byte_idx] &= !(1 << bit_idx);
        }
    }

    /// Get which side a vertex belongs to (false = A, true = B).
    #[inline]
    pub fn get_side(&self, vertex: u16) -> bool {
        if vertex >= 256 {
            return false;
        }
        let byte_idx = (vertex / 8) as usize;
        let bit_idx = vertex % 8;
        (self.side[byte_idx] >> bit_idx) & 1 != 0
    }

    /// Compute the FNV-1a hash of the side bitset.
    pub fn compute_hash(&mut self) {
        self.canonical_hash = fnv1a_hash(&self.side);
    }

    /// Check if this partition is in canonical orientation.
    ///
    /// Canonical means: side A (the cleared bits) represents the
    /// lex-smallest vertex set. Equivalently, the first set bit in
    /// the bitset must be 1 (vertex 0 is on side A) OR, if vertex 0
    /// is on side B, we should flip.
    ///
    /// More precisely: the complement of `side` (i.e. the A-set bitset)
    /// must be lex-smaller-or-equal to `side` (the B-set bitset).
    pub fn is_canonical(&self) -> bool {
        // Compare side vs. its complement byte-by-byte.
        // The complement represents side-A. If complement < side, canonical.
        // If complement > side, not canonical (should flip).
        // If equal, canonical by convention.
        for i in 0..32 {
            let complement = !self.side[i];
            if complement < self.side[i] {
                return true;
            }
            if complement > self.side[i] {
                return false;
            }
        }
        true // Equal (symmetric partition)
    }

    /// Flip the partition so that side A and side B swap.
    pub fn flip(&mut self) {
        for i in 0..32 {
            self.side[i] = !self.side[i];
        }
        let tmp = self.cardinality_a;
        self.cardinality_a = self.cardinality_b;
        self.cardinality_b = tmp;
    }

    /// Recount cardinalities from the bitset.
    pub fn recount(&mut self) {
        let mut count_b: u16 = 0;
        for i in 0..32 {
            count_b += self.side[i].count_ones() as u16;
        }
        self.cardinality_b = count_b;
        // cardinality_a is total vertices minus B, but we only know
        // about the vertices that were explicitly placed. We approximate
        // with 256 - B here; the caller may adjust.
        self.cardinality_a = 256u16.saturating_sub(count_b);
    }
}

// ============================================================================
// Canonical witness fragment
// ============================================================================

/// Canonical witness fragment (16 bytes, same as `WitnessFragment`).
///
/// Extends the original witness fragment with pseudo-deterministic
/// partition information derived from the cactus tree.
#[derive(Debug, Copy, Clone, Default)]
#[repr(C, align(16))]
pub struct CanonicalWitnessFragment {
    /// Tile ID (0-255)
    pub tile_id: u8,
    /// Truncated epoch (tick & 0xFF)
    pub epoch: u8,
    /// Vertices on side A of the canonical partition
    pub cardinality_a: u16,
    /// Vertices on side B of the canonical partition
    pub cardinality_b: u16,
    /// Cut value (original weight format, truncated)
    pub cut_value: u16,
    /// FNV-1a hash of the canonical partition bitset
    pub canonical_hash: [u8; 4],
    /// Number of boundary edges
    pub boundary_edges: u16,
    /// Truncated hash of the cactus structure
    pub cactus_digest: u16,
}

// Compile-time size assertion
const _: () = assert!(
    size_of::<CanonicalWitnessFragment>() == 16,
    "CanonicalWitnessFragment must be exactly 16 bytes"
);

// ============================================================================
// FNV-1a hash (no_std, no allocation)
// ============================================================================

/// Compute a 32-bit FNV-1a hash of the given byte slice.
///
/// FNV-1a is a simple, fast, non-cryptographic hash with good
/// distribution properties. It is fully deterministic and portable.
#[inline]
pub fn fnv1a_hash(data: &[u8]) -> [u8; 4] {
    let mut hash: u32 = 0x811c9dc5; // FNV offset basis
    for &byte in data {
        hash ^= byte as u32;
        hash = hash.wrapping_mul(0x01000193); // FNV prime
    }
    hash.to_le_bytes()
}

// ============================================================================
// Tests
// ============================================================================

#[cfg(test)]
mod tests {
    use super::*;
    use crate::shard::CompactGraph;
    use crate::TileState;
    use core::mem::size_of;

    #[test]
    fn test_fixed_point_weight_ordering() {
        let a = FixedPointWeight(100);
        let b = FixedPointWeight(200);
        let c = FixedPointWeight(100);

        assert!(a < b);
        assert!(b > a);
        assert_eq!(a, c);
        assert!(a <= c);
        assert!(a >= c);

        // Check from_u16_weight ordering
        let w1 = FixedPointWeight::from_u16_weight(50);
        let w2 = FixedPointWeight::from_u16_weight(100);
        assert!(w1 < w2);

        // Saturating add
        let sum = w1.saturating_add(w2);
        assert_eq!(sum, FixedPointWeight((50u32 << 8) + (100u32 << 8)));

        // Saturating add at max
        let max_sum = FixedPointWeight::MAX.saturating_add(FixedPointWeight::ONE);
        assert_eq!(max_sum, FixedPointWeight::MAX);
    }

    #[test]
    fn test_canonical_partition_determinism() {
        // Build the same graph twice, verify same partition hash
        let build_graph = || {
            let mut g = CompactGraph::new();
            g.add_edge(0, 1, 100);
            g.add_edge(1, 2, 100);
            g.add_edge(2, 3, 100);
            g.add_edge(3, 0, 100);
            g.add_edge(0, 2, 50); // Diagonal, lighter weight
            g.recompute_components();
            g
        };

        let g1 = build_graph();
        let g2 = build_graph();

        let c1 = ArenaCactus::build_from_compact_graph(&g1);
        let c2 = ArenaCactus::build_from_compact_graph(&g2);

        let p1 = c1.canonical_partition();
        let p2 = c2.canonical_partition();

        assert_eq!(p1.canonical_hash, p2.canonical_hash);
        assert_eq!(p1.side, p2.side);
        assert_eq!(p1.cut_value, p2.cut_value);
    }

    #[test]
    fn test_fnv1a_known_values() {
        // Empty input
        let h0 = fnv1a_hash(&[]);
        assert_eq!(
            u32::from_le_bytes(h0),
            0x811c9dc5,
            "FNV-1a of empty should be the offset basis"
        );

        // Single zero byte
        let h1 = fnv1a_hash(&[0]);
        let expected = 0x811c9dc5u32 ^ 0;
        let expected = expected.wrapping_mul(0x01000193);
        assert_eq!(u32::from_le_bytes(h1), expected);

        // Determinism: same input -> same output
        let data = [1, 2, 3, 4, 5, 6, 7, 8];
        let a = fnv1a_hash(&data);
        let b = fnv1a_hash(&data);
        assert_eq!(a, b);

        // Different input -> (almost certainly) different output
        let c = fnv1a_hash(&[8, 7, 6, 5, 4, 3, 2, 1]);
        assert_ne!(a, c);
    }

    #[test]
    fn test_arena_cactus_simple_triangle() {
        let mut g = CompactGraph::new();
        g.add_edge(0, 1, 100);
        g.add_edge(1, 2, 100);
        g.add_edge(2, 0, 100);
        g.recompute_components();

        let cactus = ArenaCactus::build_from_compact_graph(&g);

        // A triangle is 2-edge-connected, so the cactus should have
        // a single node (all 3 vertices collapsed into one component).
        assert!(
            cactus.n_nodes >= 1,
            "Triangle cactus should have at least 1 node"
        );

        // The partition should be trivial since there is only one component
        let partition = cactus.canonical_partition();
        partition.canonical_hash; // Just ensure it doesn't panic
    }

    #[test]
    fn test_canonical_witness_fragment_size() {
        assert_eq!(
            size_of::<CanonicalWitnessFragment>(),
            16,
            "CanonicalWitnessFragment must be exactly 16 bytes"
        );
    }

    #[test]
    fn test_canonical_witness_reproducibility() {
        // Build two identical tile states and verify they produce the
        // same canonical witness fragment.
        let build_tile = || {
            let mut tile = TileState::new(42);
            tile.ingest_delta(&crate::delta::Delta::edge_add(0, 1, 100));
            tile.ingest_delta(&crate::delta::Delta::edge_add(1, 2, 100));
            tile.ingest_delta(&crate::delta::Delta::edge_add(2, 3, 200));
            tile.ingest_delta(&crate::delta::Delta::edge_add(3, 0, 200));
            tile.tick(10);
            tile
        };

        let t1 = build_tile();
        let t2 = build_tile();

        let w1 = t1.canonical_witness();
        let w2 = t2.canonical_witness();

        assert_eq!(w1.tile_id, w2.tile_id);
        assert_eq!(w1.epoch, w2.epoch);
        assert_eq!(w1.cardinality_a, w2.cardinality_a);
        assert_eq!(w1.cardinality_b, w2.cardinality_b);
        assert_eq!(w1.cut_value, w2.cut_value);
        assert_eq!(w1.canonical_hash, w2.canonical_hash);
        assert_eq!(w1.boundary_edges, w2.boundary_edges);
        assert_eq!(w1.cactus_digest, w2.cactus_digest);
    }

    #[test]
    fn test_partition_set_get_side() {
        let mut p = CanonicalPartition::empty();

        // All on side A initially
        for v in 0..256u16 {
            assert!(!p.get_side(v), "vertex {} should be on side A", v);
        }

        // Set some to side B
        p.set_side(0, true);
        p.set_side(7, true);
        p.set_side(8, true);
        p.set_side(255, true);

        assert!(p.get_side(0));
        assert!(p.get_side(7));
        assert!(p.get_side(8));
        assert!(p.get_side(255));
        assert!(!p.get_side(1));
        assert!(!p.get_side(254));

        // Clear
        p.set_side(0, false);
        assert!(!p.get_side(0));
    }

    #[test]
    fn test_partition_flip() {
        let mut p = CanonicalPartition::empty();
        p.set_side(0, true);
        p.set_side(1, true);
        p.cardinality_a = 254;
        p.cardinality_b = 2;

        p.flip();

        assert!(!p.get_side(0));
        assert!(!p.get_side(1));
        assert!(p.get_side(2));
        assert_eq!(p.cardinality_a, 2);
        assert_eq!(p.cardinality_b, 254);
    }

    #[test]
    fn test_empty_graph_cactus() {
        let g = CompactGraph::new();
        let cactus = ArenaCactus::build_from_compact_graph(&g);
        assert_eq!(cactus.n_nodes, 0);
        assert_eq!(cactus.min_cut_value, FixedPointWeight::ZERO);
    }

    #[test]
    fn test_single_edge_cactus() {
        let mut g = CompactGraph::new();
        g.add_edge(0, 1, 150);
        g.recompute_components();

        let cactus = ArenaCactus::build_from_compact_graph(&g);
        assert!(cactus.n_nodes >= 2, "Single edge should have 2 cactus nodes");

        let partition = cactus.canonical_partition();
        // One vertex on each side
        assert!(
            partition.cardinality_b >= 1,
            "Should have at least 1 vertex on side B"
        );
    }
}