Skip to main content

cognitum_gate_kernel/
lib.rs

1//! Cognitum Gate Kernel
2//!
3//! A no_std WASM kernel for worker tiles in a 256-tile coherence gate fabric.
4//! Each tile maintains a local graph shard, accumulates evidence for sequential
5//! testing, and produces witness fragments for aggregation.
6//!
7//! # Architecture
8//!
9//! The coherence gate consists of 256 worker tiles, each running this kernel.
10//! Tiles receive delta updates (edge additions, removals, weight changes) and
11//! observations, process them through a deterministic tick loop, and produce
12//! reports containing:
13//!
14//! - Local graph state (vertices, edges, components)
15//! - Evidence accumulation (e-values for hypothesis testing)
16//! - Witness fragments (for global min-cut aggregation)
17//!
18//! # Memory Budget
19//!
20//! Each tile operates within a ~64KB memory budget:
21//! - CompactGraph: ~42KB (vertices, edges, adjacency)
22//! - EvidenceAccumulator: ~2KB (hypotheses, sliding window)
23//! - TileState: ~1KB (configuration, buffers)
24//! - Stack/Control: ~19KB (remaining)
25//!
26//! # WASM Exports
27//!
28//! The kernel exports three main functions for the WASM interface:
29//!
30//! - `ingest_delta`: Process incoming delta updates
31//! - `tick`: Execute one step of the deterministic tick loop
32//! - `get_witness_fragment`: Retrieve the current witness fragment
33//!
34//! # Example
35//!
36//! ```ignore
37//! // Initialize tile
38//! let tile = TileState::new(42);  // Tile ID 42
39//!
40//! // Ingest deltas
41//! tile.ingest_delta(&Delta::edge_add(0, 1, 100));
42//! tile.ingest_delta(&Delta::edge_add(1, 2, 100));
43//!
44//! // Process tick
45//! let report = tile.tick(1);
46//!
47//! // Get witness
48//! let witness = tile.get_witness_fragment();
49//! ```
50
51#![cfg_attr(not(feature = "std"), no_std)]
52#![deny(unsafe_op_in_unsafe_fn)]
53#![warn(missing_docs)]
54#![allow(clippy::missing_safety_doc)]
55
56#[cfg(not(feature = "std"))]
57extern crate alloc;
58
59// Global allocator for no_std builds
60#[cfg(all(not(feature = "std"), not(test)))]
61mod allocator {
62    use core::alloc::{GlobalAlloc, Layout};
63
64    /// A simple bump allocator for no_std WASM builds
65    /// In production, this would be replaced with wee_alloc or similar
66    struct BumpAllocator;
67
68    // 64KB heap for each tile
69    const HEAP_SIZE: usize = 65536;
70    static mut HEAP: [u8; HEAP_SIZE] = [0; HEAP_SIZE];
71    static mut HEAP_PTR: usize = 0;
72
73    unsafe impl GlobalAlloc for BumpAllocator {
74        unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
75            let size = layout.size();
76            let align = layout.align();
77
78            unsafe {
79                // Align the heap pointer
80                let aligned = (HEAP_PTR + align - 1) & !(align - 1);
81
82                if aligned + size > HEAP_SIZE {
83                    core::ptr::null_mut()
84                } else {
85                    HEAP_PTR = aligned + size;
86                    HEAP.as_mut_ptr().add(aligned)
87                }
88            }
89        }
90
91        unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {
92            // Bump allocator doesn't deallocate
93            // This is fine for short-lived WASM kernels
94        }
95    }
96
97    #[global_allocator]
98    static ALLOCATOR: BumpAllocator = BumpAllocator;
99}
100
101// Panic handler for no_std builds (not needed for tests or std builds)
102#[cfg(all(not(feature = "std"), not(test), target_arch = "wasm32"))]
103#[panic_handler]
104fn panic(_info: &core::panic::PanicInfo) -> ! {
105    // In WASM, we can use unreachable to trap
106    core::arch::wasm32::unreachable()
107}
108
109// For non-wasm no_std builds without test
110#[cfg(all(not(feature = "std"), not(test), not(target_arch = "wasm32")))]
111#[panic_handler]
112fn panic(_info: &core::panic::PanicInfo) -> ! {
113    loop {}
114}
115
116pub mod delta;
117pub mod evidence;
118pub mod report;
119pub mod shard;
120
121#[cfg(feature = "canonical-witness")]
122pub mod canonical_witness;
123
124#[cfg(feature = "canonical-witness")]
125pub use canonical_witness::{
126    ArenaCactus, CanonicalPartition, CanonicalWitnessFragment, CactusNode, FixedPointWeight,
127};
128
129use crate::delta::{Delta, DeltaTag};
130use crate::evidence::EvidenceAccumulator;
131use crate::report::{TileReport, TileStatus, WitnessFragment};
132use crate::shard::CompactGraph;
133use core::mem::size_of;
134
135/// Maximum deltas in ingestion buffer
136pub const MAX_DELTA_BUFFER: usize = 64;
137
138/// Tile state containing all local state for a worker tile
139#[repr(C)]
140pub struct TileState {
141    /// Tile identifier (0-255)
142    pub tile_id: u8,
143    /// Status flags
144    pub status: u8,
145    /// Current tick number
146    pub tick: u32,
147    /// Generation number (incremented on structural changes)
148    pub generation: u16,
149    /// Reserved padding
150    pub _reserved: [u8; 2],
151    /// Local graph shard
152    pub graph: CompactGraph,
153    /// Evidence accumulator
154    pub evidence: EvidenceAccumulator,
155    /// Delta ingestion buffer
156    pub delta_buffer: [Delta; MAX_DELTA_BUFFER],
157    /// Number of deltas in buffer
158    pub delta_count: u16,
159    /// Buffer head pointer
160    pub delta_head: u16,
161    /// Last report produced
162    pub last_report: TileReport,
163}
164
165impl TileState {
166    /// Status: tile is initialized
167    pub const STATUS_INITIALIZED: u8 = 0x01;
168    /// Status: tile has pending deltas
169    pub const STATUS_HAS_DELTAS: u8 = 0x02;
170    /// Status: tile needs recomputation
171    pub const STATUS_DIRTY: u8 = 0x04;
172    /// Status: tile is in error state
173    pub const STATUS_ERROR: u8 = 0x80;
174
175    /// Create a new tile state
176    pub fn new(tile_id: u8) -> Self {
177        Self {
178            tile_id,
179            status: Self::STATUS_INITIALIZED,
180            tick: 0,
181            generation: 0,
182            _reserved: [0; 2],
183            graph: CompactGraph::new(),
184            evidence: EvidenceAccumulator::new(),
185            delta_buffer: [Delta::nop(); MAX_DELTA_BUFFER],
186            delta_count: 0,
187            delta_head: 0,
188            last_report: TileReport::new(tile_id),
189        }
190    }
191
192    /// Ingest a delta into the buffer
193    ///
194    /// Returns true if the delta was successfully buffered.
195    /// Returns false if the buffer is full.
196    pub fn ingest_delta(&mut self, delta: &Delta) -> bool {
197        if self.delta_count as usize >= MAX_DELTA_BUFFER {
198            return false;
199        }
200
201        let idx = (self.delta_head as usize + self.delta_count as usize) % MAX_DELTA_BUFFER;
202        self.delta_buffer[idx] = *delta;
203        self.delta_count += 1;
204        self.status |= Self::STATUS_HAS_DELTAS;
205        true
206    }
207
208    /// Ingest a delta from raw bytes
209    ///
210    /// # Safety
211    ///
212    /// The caller must ensure that `ptr` points to a valid `Delta` structure
213    /// and that the pointer is properly aligned.
214    #[inline]
215    pub unsafe fn ingest_delta_raw(&mut self, ptr: *const u8) -> bool {
216        let delta = unsafe { &*(ptr as *const Delta) };
217        self.ingest_delta(delta)
218    }
219
220    /// Process one tick of the kernel
221    ///
222    /// This is the main entry point for the tick loop. It:
223    /// 1. Processes all buffered deltas
224    /// 2. Updates the evidence accumulator
225    /// 3. Recomputes graph connectivity if needed
226    /// 4. Produces a tile report
227    pub fn tick(&mut self, tick_number: u32) -> TileReport {
228        self.tick = tick_number;
229        let tick_start = self.current_time_us();
230
231        // Process buffered deltas
232        let deltas_processed = self.process_deltas();
233
234        // Recompute connectivity if graph is dirty
235        if self.graph.status & CompactGraph::STATUS_DIRTY != 0 {
236            self.graph.recompute_components();
237        }
238
239        // Build report
240        let mut report = TileReport::new(self.tile_id);
241        report.tick = tick_number;
242        report.generation = self.generation;
243        report.status = TileStatus::Complete;
244
245        // Graph state
246        report.num_vertices = self.graph.num_vertices;
247        report.num_edges = self.graph.num_edges;
248        report.num_components = self.graph.num_components;
249        report.set_connected(self.graph.is_connected());
250
251        if self.graph.status & CompactGraph::STATUS_DIRTY != 0 {
252            report.graph_flags |= TileReport::GRAPH_DIRTY;
253        }
254
255        // Evidence state
256        report.log_e_value = self.evidence.global_log_e;
257        report.obs_count = self.evidence.total_obs as u16;
258        report.rejected_count = self.evidence.rejected_count;
259
260        // Witness fragment
261        report.witness = self.compute_witness_fragment();
262
263        // Performance metrics
264        let tick_end = self.current_time_us();
265        report.tick_time_us = (tick_end - tick_start) as u16;
266        report.deltas_processed = deltas_processed as u16;
267        report.memory_kb = (Self::memory_size() / 1024) as u16;
268
269        self.last_report = report;
270        report
271    }
272
273    /// Get the current witness fragment
274    pub fn get_witness_fragment(&self) -> WitnessFragment {
275        self.last_report.witness
276    }
277
278    /// Process all buffered deltas
279    fn process_deltas(&mut self) -> usize {
280        let mut processed = 0;
281
282        while self.delta_count > 0 {
283            let delta = self.delta_buffer[self.delta_head as usize];
284            self.delta_head = ((self.delta_head as usize + 1) % MAX_DELTA_BUFFER) as u16;
285            self.delta_count -= 1;
286
287            self.apply_delta(&delta);
288            processed += 1;
289        }
290
291        self.status &= !Self::STATUS_HAS_DELTAS;
292        processed
293    }
294
295    /// Apply a single delta to the tile state
296    fn apply_delta(&mut self, delta: &Delta) {
297        match delta.tag {
298            DeltaTag::Nop => {}
299            DeltaTag::EdgeAdd => {
300                let ea = unsafe { delta.get_edge_add() };
301                self.graph.add_edge(ea.source, ea.target, ea.weight);
302                self.generation = self.generation.wrapping_add(1);
303            }
304            DeltaTag::EdgeRemove => {
305                let er = unsafe { delta.get_edge_remove() };
306                self.graph.remove_edge(er.source, er.target);
307                self.generation = self.generation.wrapping_add(1);
308            }
309            DeltaTag::WeightUpdate => {
310                let wu = unsafe { delta.get_weight_update() };
311                self.graph
312                    .update_weight(wu.source, wu.target, wu.new_weight);
313            }
314            DeltaTag::Observation => {
315                let obs = unsafe { *delta.get_observation() };
316                self.evidence.process_observation(obs, self.tick);
317            }
318            DeltaTag::BatchEnd => {
319                // Trigger recomputation
320                self.status |= Self::STATUS_DIRTY;
321            }
322            DeltaTag::Checkpoint => {
323                // TODO: Implement checkpointing
324            }
325            DeltaTag::Reset => {
326                self.graph.clear();
327                self.evidence.reset();
328                self.generation = 0;
329            }
330        }
331    }
332
333    /// Compute the witness fragment for the current state
334    fn compute_witness_fragment(&self) -> WitnessFragment {
335        // Find the vertex with minimum degree (likely on cut boundary)
336        let mut min_degree = u8::MAX;
337        let mut seed = 0u16;
338
339        for v in 0..shard::MAX_SHARD_VERTICES {
340            if self.graph.vertices[v].is_active() {
341                let degree = self.graph.vertices[v].degree;
342                if degree < min_degree && degree > 0 {
343                    min_degree = degree;
344                    seed = v as u16;
345                }
346            }
347        }
348
349        // Count boundary vertices (vertices with edges to other tiles would be marked ghost)
350        let mut boundary = 0u16;
351        for v in 0..shard::MAX_SHARD_VERTICES {
352            if self.graph.vertices[v].is_active()
353                && (self.graph.vertices[v].flags & shard::VertexEntry::FLAG_BOUNDARY) != 0
354            {
355                boundary += 1;
356            }
357        }
358
359        // Estimate local min cut as minimum vertex degree * average edge weight
360        // This is a heuristic; actual min-cut requires more computation
361        let local_min_cut = if min_degree == u8::MAX {
362            0
363        } else {
364            // Average weight (assuming uniform for simplicity)
365            min_degree as u16 * 100 // weight scale factor
366        };
367
368        let mut fragment =
369            WitnessFragment::new(seed, boundary, self.graph.num_vertices, local_min_cut);
370        fragment.component = self.graph.num_components;
371        fragment.compute_hash();
372
373        fragment
374    }
375
376    /// Get current time in microseconds (stub for no_std)
377    #[inline]
378    fn current_time_us(&self) -> u32 {
379        // In actual WASM, this would call a host function
380        // For now, return tick-based time
381        self.tick * 1000
382    }
383
384    /// Get total memory size of tile state
385    pub const fn memory_size() -> usize {
386        size_of::<Self>()
387    }
388
389    /// Reset the tile to initial state
390    pub fn reset(&mut self) {
391        self.graph.clear();
392        self.evidence.reset();
393        self.delta_count = 0;
394        self.delta_head = 0;
395        self.tick = 0;
396        self.generation = 0;
397        self.status = Self::STATUS_INITIALIZED;
398    }
399
400    /// Check if tile has pending deltas
401    #[inline]
402    pub fn has_pending_deltas(&self) -> bool {
403        self.delta_count > 0
404    }
405
406    /// Check if tile is in error state
407    #[inline]
408    pub fn is_error(&self) -> bool {
409        self.status & Self::STATUS_ERROR != 0
410    }
411
412    /// Compute a canonical witness fragment for the current tile state.
413    ///
414    /// This produces a reproducible, hash-stable 16-byte witness by:
415    /// 1. Building a cactus tree from the `CompactGraph`
416    /// 2. Deriving a canonical (lex-smallest) min-cut partition
417    /// 3. Packing the result into a `CanonicalWitnessFragment`
418    ///
419    /// Temporary stack usage: ~2.1KB (fits in the 14.5KB remaining headroom).
420    #[cfg(feature = "canonical-witness")]
421    pub fn canonical_witness(&self) -> canonical_witness::CanonicalWitnessFragment {
422        let cactus = canonical_witness::ArenaCactus::build_from_compact_graph(&self.graph);
423        let partition = cactus.canonical_partition();
424
425        canonical_witness::CanonicalWitnessFragment {
426            tile_id: self.tile_id,
427            epoch: (self.tick & 0xFF) as u8,
428            cardinality_a: partition.cardinality_a,
429            cardinality_b: partition.cardinality_b,
430            cut_value: cactus.min_cut_value.to_u16(),
431            canonical_hash: partition.canonical_hash,
432            boundary_edges: self.graph.num_edges,
433            cactus_digest: cactus.digest(),
434        }
435    }
436}
437
438// ============================================================================
439// WASM Exports
440// ============================================================================
441
442/// Global tile state (single tile per WASM instance)
443static mut TILE_STATE: Option<TileState> = None;
444
445/// Initialize the tile with the given ID
446///
447/// # Safety
448///
449/// This function modifies global state. It should only be called once
450/// during module initialization.
451#[no_mangle]
452pub unsafe extern "C" fn init_tile(tile_id: u8) {
453    unsafe {
454        TILE_STATE = Some(TileState::new(tile_id));
455    }
456}
457
458/// Ingest a delta from raw memory
459///
460/// # Safety
461///
462/// - `ptr` must point to a valid `Delta` structure
463/// - The tile must be initialized
464///
465/// Returns 1 on success, 0 if buffer is full or tile not initialized.
466#[no_mangle]
467pub unsafe extern "C" fn ingest_delta(ptr: *const u8) -> i32 {
468    unsafe {
469        match TILE_STATE.as_mut() {
470            Some(tile) => {
471                if tile.ingest_delta_raw(ptr) {
472                    1
473                } else {
474                    0
475                }
476            }
477            None => 0,
478        }
479    }
480}
481
482/// Execute one tick of the kernel
483///
484/// # Safety
485///
486/// - `report_ptr` must point to a buffer of at least 64 bytes
487/// - The tile must be initialized
488///
489/// Returns 1 on success, 0 if tile not initialized.
490#[no_mangle]
491pub unsafe extern "C" fn tick(tick_number: u32, report_ptr: *mut u8) -> i32 {
492    unsafe {
493        match TILE_STATE.as_mut() {
494            Some(tile) => {
495                let report = tile.tick(tick_number);
496                // Copy report to output buffer
497                let report_bytes =
498                    core::slice::from_raw_parts(&report as *const TileReport as *const u8, 64);
499                core::ptr::copy_nonoverlapping(report_bytes.as_ptr(), report_ptr, 64);
500                1
501            }
502            None => 0,
503        }
504    }
505}
506
507/// Get the current witness fragment
508///
509/// # Safety
510///
511/// - `fragment_ptr` must point to a buffer of at least 16 bytes
512/// - The tile must be initialized
513///
514/// Returns 1 on success, 0 if tile not initialized.
515#[no_mangle]
516pub unsafe extern "C" fn get_witness_fragment(fragment_ptr: *mut u8) -> i32 {
517    unsafe {
518        match TILE_STATE.as_ref() {
519            Some(tile) => {
520                let fragment = tile.get_witness_fragment();
521                let fragment_bytes = core::slice::from_raw_parts(
522                    &fragment as *const WitnessFragment as *const u8,
523                    16,
524                );
525                core::ptr::copy_nonoverlapping(fragment_bytes.as_ptr(), fragment_ptr, 16);
526                1
527            }
528            None => 0,
529        }
530    }
531}
532
533/// Get tile status
534///
535/// # Safety
536///
537/// The tile must be initialized.
538///
539/// Returns status byte, or 0xFF if not initialized.
540#[no_mangle]
541pub unsafe extern "C" fn get_status() -> u8 {
542    unsafe {
543        match TILE_STATE.as_ref() {
544            Some(tile) => tile.status,
545            None => 0xFF,
546        }
547    }
548}
549
550/// Reset the tile state
551///
552/// # Safety
553///
554/// The tile must be initialized.
555#[no_mangle]
556pub unsafe extern "C" fn reset_tile() {
557    unsafe {
558        if let Some(tile) = TILE_STATE.as_mut() {
559            tile.reset();
560        }
561    }
562}
563
564/// Get memory usage in bytes
565#[no_mangle]
566pub extern "C" fn get_memory_usage() -> u32 {
567    TileState::memory_size() as u32
568}
569
570// ============================================================================
571// Tests
572// ============================================================================
573
574#[cfg(test)]
575mod tests {
576    use super::*;
577    use crate::delta::Observation;
578
579    #[test]
580    fn test_tile_state_new() {
581        let tile = TileState::new(42);
582        assert_eq!(tile.tile_id, 42);
583        assert_eq!(tile.tick, 0);
584        assert_eq!(tile.delta_count, 0);
585    }
586
587    #[test]
588    fn test_ingest_delta() {
589        let mut tile = TileState::new(0);
590
591        let delta = Delta::edge_add(1, 2, 100);
592        assert!(tile.ingest_delta(&delta));
593        assert_eq!(tile.delta_count, 1);
594        assert!(tile.has_pending_deltas());
595    }
596
597    #[test]
598    fn test_ingest_buffer_full() {
599        let mut tile = TileState::new(0);
600
601        // Fill buffer
602        for i in 0..MAX_DELTA_BUFFER {
603            let delta = Delta::edge_add(i as u16, (i + 1) as u16, 100);
604            assert!(tile.ingest_delta(&delta));
605        }
606
607        // Should fail when full
608        let delta = Delta::edge_add(100, 101, 100);
609        assert!(!tile.ingest_delta(&delta));
610    }
611
612    #[test]
613    fn test_tick_processes_deltas() {
614        let mut tile = TileState::new(0);
615
616        // Add some edges
617        tile.ingest_delta(&Delta::edge_add(0, 1, 100));
618        tile.ingest_delta(&Delta::edge_add(1, 2, 100));
619        tile.ingest_delta(&Delta::edge_add(2, 0, 100));
620
621        // Process tick
622        let report = tile.tick(1);
623
624        assert_eq!(report.tile_id, 0);
625        assert_eq!(report.tick, 1);
626        assert_eq!(report.status, TileStatus::Complete);
627        assert_eq!(report.num_vertices, 3);
628        assert_eq!(report.num_edges, 3);
629        assert_eq!(report.deltas_processed, 3);
630        assert!(!tile.has_pending_deltas());
631    }
632
633    #[test]
634    fn test_tick_connectivity() {
635        let mut tile = TileState::new(0);
636
637        // Create a connected graph
638        tile.ingest_delta(&Delta::edge_add(0, 1, 100));
639        tile.ingest_delta(&Delta::edge_add(1, 2, 100));
640
641        let report = tile.tick(1);
642        assert!(report.is_connected());
643        assert_eq!(report.num_components, 1);
644    }
645
646    #[test]
647    fn test_tick_disconnected() {
648        let mut tile = TileState::new(0);
649
650        // Create two disconnected components
651        tile.ingest_delta(&Delta::edge_add(0, 1, 100));
652        tile.ingest_delta(&Delta::edge_add(2, 3, 100));
653
654        let report = tile.tick(1);
655        assert!(!report.is_connected());
656        assert_eq!(report.num_components, 2);
657    }
658
659    #[test]
660    fn test_observation_processing() {
661        let mut tile = TileState::new(0);
662
663        // Add hypothesis
664        tile.evidence.add_connectivity_hypothesis(5);
665
666        // Process observations
667        for i in 0..5 {
668            let obs = Observation::connectivity(5, true);
669            tile.ingest_delta(&Delta::observation(obs));
670            tile.tick(i);
671        }
672
673        assert!(tile.evidence.global_e_value() > 1.0);
674    }
675
676    #[test]
677    fn test_witness_fragment() {
678        let mut tile = TileState::new(0);
679
680        tile.ingest_delta(&Delta::edge_add(0, 1, 100));
681        tile.ingest_delta(&Delta::edge_add(1, 2, 100));
682        tile.ingest_delta(&Delta::edge_add(2, 0, 100));
683
684        tile.tick(1);
685        let witness = tile.get_witness_fragment();
686
687        assert!(!witness.is_empty());
688        assert_eq!(witness.cardinality, 3);
689        assert_ne!(witness.hash, 0);
690    }
691
692    #[test]
693    fn test_reset() {
694        let mut tile = TileState::new(0);
695
696        tile.ingest_delta(&Delta::edge_add(0, 1, 100));
697        tile.tick(1);
698
699        assert_eq!(tile.graph.num_edges, 1);
700
701        tile.reset();
702
703        assert_eq!(tile.graph.num_edges, 0);
704        assert_eq!(tile.graph.num_vertices, 0);
705        assert_eq!(tile.tick, 0);
706    }
707
708    #[test]
709    fn test_memory_size() {
710        let size = TileState::memory_size();
711        // Should fit in 64KB tile budget
712        assert!(size <= 65536, "TileState exceeds 64KB: {} bytes", size);
713    }
714
715    #[test]
716    fn test_edge_removal() {
717        let mut tile = TileState::new(0);
718
719        tile.ingest_delta(&Delta::edge_add(0, 1, 100));
720        tile.ingest_delta(&Delta::edge_add(1, 2, 100));
721        tile.tick(1);
722
723        assert_eq!(tile.graph.num_edges, 2);
724
725        tile.ingest_delta(&Delta::edge_remove(0, 1));
726        tile.tick(2);
727
728        assert_eq!(tile.graph.num_edges, 1);
729    }
730
731    #[test]
732    fn test_weight_update() {
733        let mut tile = TileState::new(0);
734
735        tile.ingest_delta(&Delta::edge_add(0, 1, 100));
736        tile.tick(1);
737
738        assert_eq!(tile.graph.edge_weight(0, 1), Some(100));
739
740        tile.ingest_delta(&Delta::weight_update(0, 1, 200));
741        tile.tick(2);
742
743        assert_eq!(tile.graph.edge_weight(0, 1), Some(200));
744    }
745}