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
121use crate::delta::{Delta, DeltaTag};
122use crate::evidence::EvidenceAccumulator;
123use crate::report::{TileReport, TileStatus, WitnessFragment};
124use crate::shard::CompactGraph;
125use core::mem::size_of;
126
127/// Maximum deltas in ingestion buffer
128pub const MAX_DELTA_BUFFER: usize = 64;
129
130/// Tile state containing all local state for a worker tile
131#[repr(C)]
132pub struct TileState {
133    /// Tile identifier (0-255)
134    pub tile_id: u8,
135    /// Status flags
136    pub status: u8,
137    /// Current tick number
138    pub tick: u32,
139    /// Generation number (incremented on structural changes)
140    pub generation: u16,
141    /// Reserved padding
142    pub _reserved: [u8; 2],
143    /// Local graph shard
144    pub graph: CompactGraph,
145    /// Evidence accumulator
146    pub evidence: EvidenceAccumulator,
147    /// Delta ingestion buffer
148    pub delta_buffer: [Delta; MAX_DELTA_BUFFER],
149    /// Number of deltas in buffer
150    pub delta_count: u16,
151    /// Buffer head pointer
152    pub delta_head: u16,
153    /// Last report produced
154    pub last_report: TileReport,
155}
156
157impl TileState {
158    /// Status: tile is initialized
159    pub const STATUS_INITIALIZED: u8 = 0x01;
160    /// Status: tile has pending deltas
161    pub const STATUS_HAS_DELTAS: u8 = 0x02;
162    /// Status: tile needs recomputation
163    pub const STATUS_DIRTY: u8 = 0x04;
164    /// Status: tile is in error state
165    pub const STATUS_ERROR: u8 = 0x80;
166
167    /// Create a new tile state
168    pub fn new(tile_id: u8) -> Self {
169        Self {
170            tile_id,
171            status: Self::STATUS_INITIALIZED,
172            tick: 0,
173            generation: 0,
174            _reserved: [0; 2],
175            graph: CompactGraph::new(),
176            evidence: EvidenceAccumulator::new(),
177            delta_buffer: [Delta::nop(); MAX_DELTA_BUFFER],
178            delta_count: 0,
179            delta_head: 0,
180            last_report: TileReport::new(tile_id),
181        }
182    }
183
184    /// Ingest a delta into the buffer
185    ///
186    /// Returns true if the delta was successfully buffered.
187    /// Returns false if the buffer is full.
188    pub fn ingest_delta(&mut self, delta: &Delta) -> bool {
189        if self.delta_count as usize >= MAX_DELTA_BUFFER {
190            return false;
191        }
192
193        let idx = (self.delta_head as usize + self.delta_count as usize) % MAX_DELTA_BUFFER;
194        self.delta_buffer[idx] = *delta;
195        self.delta_count += 1;
196        self.status |= Self::STATUS_HAS_DELTAS;
197        true
198    }
199
200    /// Ingest a delta from raw bytes
201    ///
202    /// # Safety
203    ///
204    /// The caller must ensure that `ptr` points to a valid `Delta` structure
205    /// and that the pointer is properly aligned.
206    #[inline]
207    pub unsafe fn ingest_delta_raw(&mut self, ptr: *const u8) -> bool {
208        let delta = unsafe { &*(ptr as *const Delta) };
209        self.ingest_delta(delta)
210    }
211
212    /// Process one tick of the kernel
213    ///
214    /// This is the main entry point for the tick loop. It:
215    /// 1. Processes all buffered deltas
216    /// 2. Updates the evidence accumulator
217    /// 3. Recomputes graph connectivity if needed
218    /// 4. Produces a tile report
219    pub fn tick(&mut self, tick_number: u32) -> TileReport {
220        self.tick = tick_number;
221        let tick_start = self.current_time_us();
222
223        // Process buffered deltas
224        let deltas_processed = self.process_deltas();
225
226        // Recompute connectivity if graph is dirty
227        if self.graph.status & CompactGraph::STATUS_DIRTY != 0 {
228            self.graph.recompute_components();
229        }
230
231        // Build report
232        let mut report = TileReport::new(self.tile_id);
233        report.tick = tick_number;
234        report.generation = self.generation;
235        report.status = TileStatus::Complete;
236
237        // Graph state
238        report.num_vertices = self.graph.num_vertices;
239        report.num_edges = self.graph.num_edges;
240        report.num_components = self.graph.num_components;
241        report.set_connected(self.graph.is_connected());
242
243        if self.graph.status & CompactGraph::STATUS_DIRTY != 0 {
244            report.graph_flags |= TileReport::GRAPH_DIRTY;
245        }
246
247        // Evidence state
248        report.log_e_value = self.evidence.global_log_e;
249        report.obs_count = self.evidence.total_obs as u16;
250        report.rejected_count = self.evidence.rejected_count;
251
252        // Witness fragment
253        report.witness = self.compute_witness_fragment();
254
255        // Performance metrics
256        let tick_end = self.current_time_us();
257        report.tick_time_us = (tick_end - tick_start) as u16;
258        report.deltas_processed = deltas_processed as u16;
259        report.memory_kb = (Self::memory_size() / 1024) as u16;
260
261        self.last_report = report;
262        report
263    }
264
265    /// Get the current witness fragment
266    pub fn get_witness_fragment(&self) -> WitnessFragment {
267        self.last_report.witness
268    }
269
270    /// Process all buffered deltas
271    fn process_deltas(&mut self) -> usize {
272        let mut processed = 0;
273
274        while self.delta_count > 0 {
275            let delta = self.delta_buffer[self.delta_head as usize];
276            self.delta_head = ((self.delta_head as usize + 1) % MAX_DELTA_BUFFER) as u16;
277            self.delta_count -= 1;
278
279            self.apply_delta(&delta);
280            processed += 1;
281        }
282
283        self.status &= !Self::STATUS_HAS_DELTAS;
284        processed
285    }
286
287    /// Apply a single delta to the tile state
288    fn apply_delta(&mut self, delta: &Delta) {
289        match delta.tag {
290            DeltaTag::Nop => {}
291            DeltaTag::EdgeAdd => {
292                let ea = unsafe { delta.get_edge_add() };
293                self.graph.add_edge(ea.source, ea.target, ea.weight);
294                self.generation = self.generation.wrapping_add(1);
295            }
296            DeltaTag::EdgeRemove => {
297                let er = unsafe { delta.get_edge_remove() };
298                self.graph.remove_edge(er.source, er.target);
299                self.generation = self.generation.wrapping_add(1);
300            }
301            DeltaTag::WeightUpdate => {
302                let wu = unsafe { delta.get_weight_update() };
303                self.graph.update_weight(wu.source, wu.target, wu.new_weight);
304            }
305            DeltaTag::Observation => {
306                let obs = unsafe { *delta.get_observation() };
307                self.evidence.process_observation(obs, self.tick);
308            }
309            DeltaTag::BatchEnd => {
310                // Trigger recomputation
311                self.status |= Self::STATUS_DIRTY;
312            }
313            DeltaTag::Checkpoint => {
314                // TODO: Implement checkpointing
315            }
316            DeltaTag::Reset => {
317                self.graph.clear();
318                self.evidence.reset();
319                self.generation = 0;
320            }
321        }
322    }
323
324    /// Compute the witness fragment for the current state
325    fn compute_witness_fragment(&self) -> WitnessFragment {
326        // Find the vertex with minimum degree (likely on cut boundary)
327        let mut min_degree = u8::MAX;
328        let mut seed = 0u16;
329
330        for v in 0..shard::MAX_SHARD_VERTICES {
331            if self.graph.vertices[v].is_active() {
332                let degree = self.graph.vertices[v].degree;
333                if degree < min_degree && degree > 0 {
334                    min_degree = degree;
335                    seed = v as u16;
336                }
337            }
338        }
339
340        // Count boundary vertices (vertices with edges to other tiles would be marked ghost)
341        let mut boundary = 0u16;
342        for v in 0..shard::MAX_SHARD_VERTICES {
343            if self.graph.vertices[v].is_active()
344                && (self.graph.vertices[v].flags & shard::VertexEntry::FLAG_BOUNDARY) != 0
345            {
346                boundary += 1;
347            }
348        }
349
350        // Estimate local min cut as minimum vertex degree * average edge weight
351        // This is a heuristic; actual min-cut requires more computation
352        let local_min_cut = if min_degree == u8::MAX {
353            0
354        } else {
355            // Average weight (assuming uniform for simplicity)
356            min_degree as u16 * 100 // weight scale factor
357        };
358
359        let mut fragment = WitnessFragment::new(
360            seed,
361            boundary,
362            self.graph.num_vertices,
363            local_min_cut,
364        );
365        fragment.component = self.graph.num_components;
366        fragment.compute_hash();
367
368        fragment
369    }
370
371    /// Get current time in microseconds (stub for no_std)
372    #[inline]
373    fn current_time_us(&self) -> u32 {
374        // In actual WASM, this would call a host function
375        // For now, return tick-based time
376        self.tick * 1000
377    }
378
379    /// Get total memory size of tile state
380    pub const fn memory_size() -> usize {
381        size_of::<Self>()
382    }
383
384    /// Reset the tile to initial state
385    pub fn reset(&mut self) {
386        self.graph.clear();
387        self.evidence.reset();
388        self.delta_count = 0;
389        self.delta_head = 0;
390        self.tick = 0;
391        self.generation = 0;
392        self.status = Self::STATUS_INITIALIZED;
393    }
394
395    /// Check if tile has pending deltas
396    #[inline]
397    pub fn has_pending_deltas(&self) -> bool {
398        self.delta_count > 0
399    }
400
401    /// Check if tile is in error state
402    #[inline]
403    pub fn is_error(&self) -> bool {
404        self.status & Self::STATUS_ERROR != 0
405    }
406}
407
408// ============================================================================
409// WASM Exports
410// ============================================================================
411
412/// Global tile state (single tile per WASM instance)
413static mut TILE_STATE: Option<TileState> = None;
414
415/// Initialize the tile with the given ID
416///
417/// # Safety
418///
419/// This function modifies global state. It should only be called once
420/// during module initialization.
421#[no_mangle]
422pub unsafe extern "C" fn init_tile(tile_id: u8) {
423    unsafe {
424        TILE_STATE = Some(TileState::new(tile_id));
425    }
426}
427
428/// Ingest a delta from raw memory
429///
430/// # Safety
431///
432/// - `ptr` must point to a valid `Delta` structure
433/// - The tile must be initialized
434///
435/// Returns 1 on success, 0 if buffer is full or tile not initialized.
436#[no_mangle]
437pub unsafe extern "C" fn ingest_delta(ptr: *const u8) -> i32 {
438    unsafe {
439        match TILE_STATE.as_mut() {
440            Some(tile) => {
441                if tile.ingest_delta_raw(ptr) {
442                    1
443                } else {
444                    0
445                }
446            }
447            None => 0,
448        }
449    }
450}
451
452/// Execute one tick of the kernel
453///
454/// # Safety
455///
456/// - `report_ptr` must point to a buffer of at least 64 bytes
457/// - The tile must be initialized
458///
459/// Returns 1 on success, 0 if tile not initialized.
460#[no_mangle]
461pub unsafe extern "C" fn tick(tick_number: u32, report_ptr: *mut u8) -> i32 {
462    unsafe {
463        match TILE_STATE.as_mut() {
464            Some(tile) => {
465                let report = tile.tick(tick_number);
466                // Copy report to output buffer
467                let report_bytes =
468                    core::slice::from_raw_parts(&report as *const TileReport as *const u8, 64);
469                core::ptr::copy_nonoverlapping(report_bytes.as_ptr(), report_ptr, 64);
470                1
471            }
472            None => 0,
473        }
474    }
475}
476
477/// Get the current witness fragment
478///
479/// # Safety
480///
481/// - `fragment_ptr` must point to a buffer of at least 16 bytes
482/// - The tile must be initialized
483///
484/// Returns 1 on success, 0 if tile not initialized.
485#[no_mangle]
486pub unsafe extern "C" fn get_witness_fragment(fragment_ptr: *mut u8) -> i32 {
487    unsafe {
488        match TILE_STATE.as_ref() {
489            Some(tile) => {
490                let fragment = tile.get_witness_fragment();
491                let fragment_bytes = core::slice::from_raw_parts(
492                    &fragment as *const WitnessFragment as *const u8,
493                    16,
494                );
495                core::ptr::copy_nonoverlapping(fragment_bytes.as_ptr(), fragment_ptr, 16);
496                1
497            }
498            None => 0,
499        }
500    }
501}
502
503/// Get tile status
504///
505/// # Safety
506///
507/// The tile must be initialized.
508///
509/// Returns status byte, or 0xFF if not initialized.
510#[no_mangle]
511pub unsafe extern "C" fn get_status() -> u8 {
512    unsafe {
513        match TILE_STATE.as_ref() {
514            Some(tile) => tile.status,
515            None => 0xFF,
516        }
517    }
518}
519
520/// Reset the tile state
521///
522/// # Safety
523///
524/// The tile must be initialized.
525#[no_mangle]
526pub unsafe extern "C" fn reset_tile() {
527    unsafe {
528        if let Some(tile) = TILE_STATE.as_mut() {
529            tile.reset();
530        }
531    }
532}
533
534/// Get memory usage in bytes
535#[no_mangle]
536pub extern "C" fn get_memory_usage() -> u32 {
537    TileState::memory_size() as u32
538}
539
540// ============================================================================
541// Tests
542// ============================================================================
543
544#[cfg(test)]
545mod tests {
546    use super::*;
547    use crate::delta::Observation;
548
549    #[test]
550    fn test_tile_state_new() {
551        let tile = TileState::new(42);
552        assert_eq!(tile.tile_id, 42);
553        assert_eq!(tile.tick, 0);
554        assert_eq!(tile.delta_count, 0);
555    }
556
557    #[test]
558    fn test_ingest_delta() {
559        let mut tile = TileState::new(0);
560
561        let delta = Delta::edge_add(1, 2, 100);
562        assert!(tile.ingest_delta(&delta));
563        assert_eq!(tile.delta_count, 1);
564        assert!(tile.has_pending_deltas());
565    }
566
567    #[test]
568    fn test_ingest_buffer_full() {
569        let mut tile = TileState::new(0);
570
571        // Fill buffer
572        for i in 0..MAX_DELTA_BUFFER {
573            let delta = Delta::edge_add(i as u16, (i + 1) as u16, 100);
574            assert!(tile.ingest_delta(&delta));
575        }
576
577        // Should fail when full
578        let delta = Delta::edge_add(100, 101, 100);
579        assert!(!tile.ingest_delta(&delta));
580    }
581
582    #[test]
583    fn test_tick_processes_deltas() {
584        let mut tile = TileState::new(0);
585
586        // Add some edges
587        tile.ingest_delta(&Delta::edge_add(0, 1, 100));
588        tile.ingest_delta(&Delta::edge_add(1, 2, 100));
589        tile.ingest_delta(&Delta::edge_add(2, 0, 100));
590
591        // Process tick
592        let report = tile.tick(1);
593
594        assert_eq!(report.tile_id, 0);
595        assert_eq!(report.tick, 1);
596        assert_eq!(report.status, TileStatus::Complete);
597        assert_eq!(report.num_vertices, 3);
598        assert_eq!(report.num_edges, 3);
599        assert_eq!(report.deltas_processed, 3);
600        assert!(!tile.has_pending_deltas());
601    }
602
603    #[test]
604    fn test_tick_connectivity() {
605        let mut tile = TileState::new(0);
606
607        // Create a connected graph
608        tile.ingest_delta(&Delta::edge_add(0, 1, 100));
609        tile.ingest_delta(&Delta::edge_add(1, 2, 100));
610
611        let report = tile.tick(1);
612        assert!(report.is_connected());
613        assert_eq!(report.num_components, 1);
614    }
615
616    #[test]
617    fn test_tick_disconnected() {
618        let mut tile = TileState::new(0);
619
620        // Create two disconnected components
621        tile.ingest_delta(&Delta::edge_add(0, 1, 100));
622        tile.ingest_delta(&Delta::edge_add(2, 3, 100));
623
624        let report = tile.tick(1);
625        assert!(!report.is_connected());
626        assert_eq!(report.num_components, 2);
627    }
628
629    #[test]
630    fn test_observation_processing() {
631        let mut tile = TileState::new(0);
632
633        // Add hypothesis
634        tile.evidence.add_connectivity_hypothesis(5);
635
636        // Process observations
637        for i in 0..5 {
638            let obs = Observation::connectivity(5, true);
639            tile.ingest_delta(&Delta::observation(obs));
640            tile.tick(i);
641        }
642
643        assert!(tile.evidence.global_e_value() > 1.0);
644    }
645
646    #[test]
647    fn test_witness_fragment() {
648        let mut tile = TileState::new(0);
649
650        tile.ingest_delta(&Delta::edge_add(0, 1, 100));
651        tile.ingest_delta(&Delta::edge_add(1, 2, 100));
652        tile.ingest_delta(&Delta::edge_add(2, 0, 100));
653
654        tile.tick(1);
655        let witness = tile.get_witness_fragment();
656
657        assert!(!witness.is_empty());
658        assert_eq!(witness.cardinality, 3);
659        assert_ne!(witness.hash, 0);
660    }
661
662    #[test]
663    fn test_reset() {
664        let mut tile = TileState::new(0);
665
666        tile.ingest_delta(&Delta::edge_add(0, 1, 100));
667        tile.tick(1);
668
669        assert_eq!(tile.graph.num_edges, 1);
670
671        tile.reset();
672
673        assert_eq!(tile.graph.num_edges, 0);
674        assert_eq!(tile.graph.num_vertices, 0);
675        assert_eq!(tile.tick, 0);
676    }
677
678    #[test]
679    fn test_memory_size() {
680        let size = TileState::memory_size();
681        // Should fit in 64KB tile budget
682        assert!(size <= 65536, "TileState exceeds 64KB: {} bytes", size);
683    }
684
685    #[test]
686    fn test_edge_removal() {
687        let mut tile = TileState::new(0);
688
689        tile.ingest_delta(&Delta::edge_add(0, 1, 100));
690        tile.ingest_delta(&Delta::edge_add(1, 2, 100));
691        tile.tick(1);
692
693        assert_eq!(tile.graph.num_edges, 2);
694
695        tile.ingest_delta(&Delta::edge_remove(0, 1));
696        tile.tick(2);
697
698        assert_eq!(tile.graph.num_edges, 1);
699    }
700
701    #[test]
702    fn test_weight_update() {
703        let mut tile = TileState::new(0);
704
705        tile.ingest_delta(&Delta::edge_add(0, 1, 100));
706        tile.tick(1);
707
708        assert_eq!(tile.graph.edge_weight(0, 1), Some(100));
709
710        tile.ingest_delta(&Delta::weight_update(0, 1, 200));
711        tile.tick(2);
712
713        assert_eq!(tile.graph.edge_weight(0, 1), Some(200));
714    }
715}