1#![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#[cfg(all(not(feature = "std"), not(test)))]
61mod allocator {
62 use core::alloc::{GlobalAlloc, Layout};
63
64 struct BumpAllocator;
67
68 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 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 }
95 }
96
97 #[global_allocator]
98 static ALLOCATOR: BumpAllocator = BumpAllocator;
99}
100
101#[cfg(all(not(feature = "std"), not(test), target_arch = "wasm32"))]
103#[panic_handler]
104fn panic(_info: &core::panic::PanicInfo) -> ! {
105 core::arch::wasm32::unreachable()
107}
108
109#[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
135pub const MAX_DELTA_BUFFER: usize = 64;
137
138#[repr(C)]
140pub struct TileState {
141 pub tile_id: u8,
143 pub status: u8,
145 pub tick: u32,
147 pub generation: u16,
149 pub _reserved: [u8; 2],
151 pub graph: CompactGraph,
153 pub evidence: EvidenceAccumulator,
155 pub delta_buffer: [Delta; MAX_DELTA_BUFFER],
157 pub delta_count: u16,
159 pub delta_head: u16,
161 pub last_report: TileReport,
163}
164
165impl TileState {
166 pub const STATUS_INITIALIZED: u8 = 0x01;
168 pub const STATUS_HAS_DELTAS: u8 = 0x02;
170 pub const STATUS_DIRTY: u8 = 0x04;
172 pub const STATUS_ERROR: u8 = 0x80;
174
175 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 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 #[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 pub fn tick(&mut self, tick_number: u32) -> TileReport {
228 self.tick = tick_number;
229 let tick_start = self.current_time_us();
230
231 let deltas_processed = self.process_deltas();
233
234 if self.graph.status & CompactGraph::STATUS_DIRTY != 0 {
236 self.graph.recompute_components();
237 }
238
239 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 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 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 report.witness = self.compute_witness_fragment();
262
263 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 pub fn get_witness_fragment(&self) -> WitnessFragment {
275 self.last_report.witness
276 }
277
278 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 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 self.status |= Self::STATUS_DIRTY;
321 }
322 DeltaTag::Checkpoint => {
323 }
325 DeltaTag::Reset => {
326 self.graph.clear();
327 self.evidence.reset();
328 self.generation = 0;
329 }
330 }
331 }
332
333 fn compute_witness_fragment(&self) -> WitnessFragment {
335 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 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 let local_min_cut = if min_degree == u8::MAX {
362 0
363 } else {
364 min_degree as u16 * 100 };
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 #[inline]
378 fn current_time_us(&self) -> u32 {
379 self.tick * 1000
382 }
383
384 pub const fn memory_size() -> usize {
386 size_of::<Self>()
387 }
388
389 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 #[inline]
402 pub fn has_pending_deltas(&self) -> bool {
403 self.delta_count > 0
404 }
405
406 #[inline]
408 pub fn is_error(&self) -> bool {
409 self.status & Self::STATUS_ERROR != 0
410 }
411
412 #[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
438static mut TILE_STATE: Option<TileState> = None;
444
445#[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#[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#[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 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#[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#[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#[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#[no_mangle]
566pub extern "C" fn get_memory_usage() -> u32 {
567 TileState::memory_size() as u32
568}
569
570#[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 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 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 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 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 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 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 tile.evidence.add_connectivity_hypothesis(5);
665
666 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 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}