1use vyre_primitives::bitset::bitset_words;
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub struct LinearDomain {
14 element_count: u32,
15}
16
17impl LinearDomain {
18 #[inline]
20 #[must_use]
21 pub const fn new(element_count: u32) -> Self {
22 Self { element_count }
23 }
24
25 #[inline]
27 #[must_use]
28 pub const fn element_count(self) -> u32 {
29 self.element_count
30 }
31
32 #[inline]
34 #[must_use]
35 pub fn bitset_words(self) -> u32 {
36 bitset_words(self.element_count)
37 }
38
39 pub fn scalar_slots(self, stage: &str, slots_per_element: u32) -> Result<u32, String> {
42 self.element_count
43 .checked_mul(slots_per_element)
44 .map(|slots| slots.max(1))
45 .ok_or_else(|| {
46 format!(
47 "{stage} element_count={} with {slots_per_element} slot(s) per element overflows u32 buffer slots. Fix: shard the analysis domain before building the GPU Program.",
48 self.element_count
49 )
50 })
51 }
52}
53
54#[derive(Clone, Copy, Debug, PartialEq)]
56pub struct CsrGraph<'a> {
57 pub node_count: u32,
59 pub edge_offsets: &'a [u32],
61 pub edge_targets: &'a [u32],
63 pub edge_kind_mask: &'a [u32],
65}
66
67impl<'a> CsrGraph<'a> {
68 #[inline]
70 #[must_use]
71 pub fn new(
72 node_count: u32,
73 edge_offsets: &'a [u32],
74 edge_targets: &'a [u32],
75 edge_kind_mask: &'a [u32],
76 ) -> Self {
77 Self {
78 node_count,
79 edge_offsets,
80 edge_targets,
81 edge_kind_mask,
82 }
83 }
84
85 pub fn validate(self, stage: &str) -> Result<Self, String> {
87 crate::dispatch_decode::require_csr_shape(
88 stage,
89 self.node_count,
90 self.edge_offsets,
91 self.edge_targets,
92 self.edge_kind_mask,
93 )?;
94 Ok(self)
95 }
96
97 pub fn normalize(self, stage: &str) -> Result<NormalizedCsrGraph, String> {
99 let mut normalized = NormalizedCsrGraph::empty();
100 let mut scratch = CsrGraphNormalizationScratch::new();
101 self.normalize_with_edge_kind_map_into(
102 stage,
103 false,
104 |mask| mask,
105 &mut normalized,
106 &mut scratch,
107 )?;
108 Ok(normalized)
109 }
110
111 pub fn normalize_with_edge_kind_mask(
117 self,
118 stage: &str,
119 allowed_mask: u32,
120 ) -> Result<NormalizedCsrGraph, String> {
121 let mut normalized = NormalizedCsrGraph::empty();
122 let mut scratch = CsrGraphNormalizationScratch::new();
123 self.normalize_with_edge_kind_map_into(
124 stage,
125 true,
126 |mask| mask & allowed_mask,
127 &mut normalized,
128 &mut scratch,
129 )?;
130 Ok(normalized)
131 }
132
133 pub fn normalize_into(
135 self,
136 stage: &str,
137 output: &mut NormalizedCsrGraph,
138 scratch: &mut CsrGraphNormalizationScratch,
139 ) -> Result<(), String> {
140 self.normalize_with_edge_kind_map_into(stage, false, |mask| mask, output, scratch)
141 }
142
143 pub fn normalize_with_edge_kind_mask_into(
145 self,
146 stage: &str,
147 allowed_mask: u32,
148 output: &mut NormalizedCsrGraph,
149 scratch: &mut CsrGraphNormalizationScratch,
150 ) -> Result<(), String> {
151 self.normalize_with_edge_kind_map_into(
152 stage,
153 true,
154 |mask| mask & allowed_mask,
155 output,
156 scratch,
157 )
158 }
159
160 fn normalize_with_edge_kind_map_into<F>(
161 self,
162 stage: &str,
163 drop_zero_mapped_edges: bool,
164 mut map_edge_kind: F,
165 output: &mut NormalizedCsrGraph,
166 scratch: &mut CsrGraphNormalizationScratch,
167 ) -> Result<(), String>
168 where
169 F: FnMut(u32) -> u32,
170 {
171 self.validate(stage)?;
172 let nodes = usize::try_from(self.node_count).map_err(|_| {
173 format!("{stage} node_count={} cannot fit usize during CSR normalization. Fix: shard the graph before dispatch.", self.node_count)
174 })?;
175 let offset_capacity = nodes.checked_add(1).ok_or_else(|| {
176 format!(
177 "{stage} node_count={} overflows CSR offset capacity. Fix: shard the graph before dispatch.",
178 self.node_count
179 )
180 })?;
181 output.clear();
182 crate::staging_reserve::reserve_vec(
183 &mut output.edge_offsets,
184 offset_capacity,
185 "CSR normalized row offset",
186 )?;
187 crate::staging_reserve::reserve_vec(
188 &mut output.edge_targets,
189 self.edge_targets.len(),
190 "CSR normalized edge target",
191 )?;
192 crate::staging_reserve::reserve_vec(
193 &mut output.edge_kind_mask,
194 self.edge_kind_mask.len(),
195 "CSR normalized edge-kind mask",
196 )?;
197 output.edge_offsets.push(0);
198 for node in 0..nodes {
199 let start = crate::dispatch_decode::u32_to_usize(
200 self.edge_offsets[node],
201 "CSR normalization row start offset",
202 )?;
203 let end = crate::dispatch_decode::u32_to_usize(
204 self.edge_offsets[node + 1],
205 "CSR normalization row end offset",
206 )?;
207 let row_len = end.checked_sub(start).ok_or_else(|| {
208 format!(
209 "{stage} CSR offsets decreased at node {node}: start={start}, end={end}. Fix: pass monotonically increasing edge offsets."
210 )
211 })?;
212 if row_len <= 1 {
213 if start != end {
214 let mapped = map_edge_kind(self.edge_kind_mask[start]);
215 if !(drop_zero_mapped_edges && mapped == 0) {
216 output.edge_targets.push(self.edge_targets[start]);
217 output.edge_kind_mask.push(mapped);
218 }
219 }
220 let offset = u32::try_from(output.edge_targets.len()).map_err(|error| {
221 format!(
222 "{stage} normalized CSR row {node} offset does not fit u32: {error}. Fix: shard the ProgramGraph before GPU dispatch."
223 )
224 })?;
225 output.edge_offsets.push(offset);
226 continue;
227 }
228 scratch.row.clear();
229 let output_row_start = output.edge_targets.len();
230 let mut previous_direct_target = None;
231 let mut direct_sorted_unique = true;
232 for (&target, &mask) in self.edge_targets[start..end]
233 .iter()
234 .zip(self.edge_kind_mask[start..end].iter())
235 {
236 let mapped = map_edge_kind(mask);
237 if drop_zero_mapped_edges && mapped == 0 {
238 continue;
239 }
240 if let Some(previous) = previous_direct_target {
241 if target <= previous {
242 direct_sorted_unique = false;
243 }
244 }
245 previous_direct_target = Some(target);
246 output.edge_targets.push(target);
247 output.edge_kind_mask.push(mapped);
248 }
249 if direct_sorted_unique {
250 let offset = u32::try_from(output.edge_targets.len()).map_err(|error| {
251 format!(
252 "{stage} normalized CSR row {node} offset does not fit u32: {error}. Fix: shard the ProgramGraph before GPU dispatch."
253 )
254 })?;
255 output.edge_offsets.push(offset);
256 continue;
257 }
258 let output_row_len =
259 output.edge_targets.len().checked_sub(output_row_start).ok_or_else(|| {
260 format!(
261 "{stage} normalized CSR output row start exceeded target length at node {node}. Fix: rebuild the graph normalization state before dispatch."
262 )
263 })?;
264 crate::staging_reserve::reserve_vec(
265 &mut scratch.row,
266 output_row_len,
267 "CSR graph normalization row scratch",
268 )?;
269 scratch.row.extend(
270 output.edge_targets[output_row_start..]
271 .iter()
272 .copied()
273 .zip(output.edge_kind_mask[output_row_start..].iter().copied()),
274 );
275 output.edge_targets.truncate(output_row_start);
276 output.edge_kind_mask.truncate(output_row_start);
277 scratch.row.sort_unstable_by_key(|(target, _)| *target);
278 let mut merged = 0usize;
279 for idx in 0..scratch.row.len() {
280 if merged != 0 && scratch.row[merged - 1].0 == scratch.row[idx].0 {
281 scratch.row[merged - 1].1 |= scratch.row[idx].1;
282 } else {
283 scratch.row[merged] = scratch.row[idx];
284 merged += 1;
285 }
286 }
287 output
288 .edge_targets
289 .extend(scratch.row[..merged].iter().map(|(target, _)| *target));
290 output
291 .edge_kind_mask
292 .extend(scratch.row[..merged].iter().map(|(_, mask)| *mask));
293 let offset = u32::try_from(output.edge_targets.len()).map_err(|error| {
294 format!(
295 "{stage} normalized CSR row {node} offset does not fit u32: {error}. Fix: shard the ProgramGraph before GPU dispatch."
296 )
297 })?;
298 output.edge_offsets.push(offset);
299 }
300 let edge_count = u32::try_from(output.edge_targets.len()).map_err(|error| {
301 format!(
302 "{stage} normalized edge target count does not fit u32: {error}. Fix: shard the ProgramGraph before GPU dispatch."
303 )
304 })?;
305 output.node_count = self.node_count;
306 output.edge_count = edge_count;
307 output.stable_layout_hash = stable_csr_layout_hash(
308 self.node_count,
309 edge_count,
310 &output.edge_offsets,
311 &output.edge_targets,
312 &output.edge_kind_mask,
313 );
314 Ok(())
315 }
316}
317
318#[derive(Clone, Debug, Default, Eq, PartialEq)]
320pub struct CsrGraphNormalizationScratch {
321 row: Vec<(u32, u32)>,
322}
323
324impl CsrGraphNormalizationScratch {
325 #[inline]
327 #[must_use]
328 pub const fn new() -> Self {
329 Self { row: Vec::new() }
330 }
331
332 #[inline]
334 #[must_use]
335 #[cfg(any(test, feature = "legacy-infallible"))]
336 pub fn with_row_capacity(row_capacity: usize) -> Self {
337 Self::try_with_row_capacity(row_capacity)
338 .expect("CSR graph normalization scratch allocation failed. Fix: shard the graph before preparing fixed-point analysis state.")
339 }
340
341 #[inline]
343 pub fn try_with_row_capacity(row_capacity: usize) -> Result<Self, String> {
344 let mut scratch = Self::new();
345 crate::staging_reserve::reserve_vec(
346 &mut scratch.row,
347 row_capacity,
348 "CSR graph normalization row scratch",
349 )?;
350 Ok(scratch)
351 }
352
353 #[inline]
355 #[must_use]
356 pub fn row_capacity(&self) -> usize {
357 self.row.capacity()
358 }
359}
360
361#[derive(Clone, Debug, Eq, PartialEq)]
363pub struct NormalizedCsrGraph {
364 node_count: u32,
365 edge_count: u32,
366 stable_layout_hash: u64,
367 edge_offsets: Vec<u32>,
368 edge_targets: Vec<u32>,
369 edge_kind_mask: Vec<u32>,
370}
371
372impl NormalizedCsrGraph {
373 #[inline]
375 #[must_use]
376 pub const fn empty() -> Self {
377 Self {
378 node_count: 0,
379 edge_count: 0,
380 stable_layout_hash: 0,
381 edge_offsets: Vec::new(),
382 edge_targets: Vec::new(),
383 edge_kind_mask: Vec::new(),
384 }
385 }
386
387 #[inline]
388 fn clear(&mut self) {
389 self.node_count = 0;
390 self.edge_count = 0;
391 self.stable_layout_hash = 0;
392 self.edge_offsets.clear();
393 self.edge_targets.clear();
394 self.edge_kind_mask.clear();
395 }
396
397 #[inline]
399 #[must_use]
400 pub fn node_count(&self) -> u32 {
401 self.node_count
402 }
403
404 #[inline]
406 #[must_use]
407 pub fn edge_count(&self) -> u32 {
408 self.edge_count
409 }
410
411 #[inline]
413 #[must_use]
414 pub fn edge_offsets(&self) -> &[u32] {
415 &self.edge_offsets
416 }
417
418 #[inline]
420 #[must_use]
421 pub fn edge_targets(&self) -> &[u32] {
422 &self.edge_targets
423 }
424
425 #[inline]
427 #[must_use]
428 pub fn edge_kind_mask(&self) -> &[u32] {
429 &self.edge_kind_mask
430 }
431
432 #[inline]
434 #[must_use]
435 pub fn edge_offsets_capacity(&self) -> usize {
436 self.edge_offsets.capacity()
437 }
438
439 #[inline]
441 #[must_use]
442 pub fn edge_targets_capacity(&self) -> usize {
443 self.edge_targets.capacity()
444 }
445
446 #[inline]
448 #[must_use]
449 pub fn edge_kind_mask_capacity(&self) -> usize {
450 self.edge_kind_mask.capacity()
451 }
452
453 #[inline]
460 #[must_use]
461 pub fn stable_layout_hash(&self) -> u64 {
462 self.stable_layout_hash
463 }
464}
465
466const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
467const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
468
469#[inline]
470fn mix_layout_bytes(mut hash: u64, bytes: &[u8]) -> u64 {
471 for byte in bytes {
472 hash ^= u64::from(*byte);
473 hash = hash.wrapping_mul(FNV_PRIME);
474 }
475 hash
476}
477
478#[inline]
479fn mix_layout_words(hash: u64, words: &[u32]) -> u64 {
480 #[cfg(target_endian = "little")]
481 {
482 mix_layout_bytes(hash, bytemuck::cast_slice(words))
483 }
484 #[cfg(target_endian = "big")]
485 {
486 for word in words {
487 hash = mix_layout_bytes(hash, &word.to_le_bytes());
488 }
489 hash
490 }
491}
492
493#[must_use]
495pub fn stable_csr_layout_hash(
496 node_count: u32,
497 edge_count: u32,
498 edge_offsets: &[u32],
499 edge_targets: &[u32],
500 edge_kind_mask: &[u32],
501) -> u64 {
502 let mut hash = FNV_OFFSET;
503 hash = mix_layout_bytes(hash, &node_count.to_le_bytes());
504 hash = mix_layout_bytes(hash, &edge_count.to_le_bytes());
505 hash = mix_layout_words(hash, edge_offsets);
506 hash = mix_layout_words(hash, edge_targets);
507 mix_layout_words(hash, edge_kind_mask)
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513
514 #[test]
515 fn linear_domain_computes_bitset_and_scalar_slots() {
516 let domain = LinearDomain::new(65);
517
518 assert_eq!(domain.element_count(), 65);
519 assert_eq!(domain.bitset_words(), 3);
520 assert_eq!(
521 domain
522 .scalar_slots("shared linear domain scalar test", 2)
523 .expect("scalar slots must fit"),
524 130
525 );
526 }
527
528 #[test]
529 fn linear_domain_rejects_scalar_slot_overflow() {
530 let err = LinearDomain::new(u32::MAX)
531 .scalar_slots("shared linear domain overflow test", 2)
532 .expect_err("slot multiplication must fail loudly");
533 assert!(err.contains("overflows"), "unexpected diagnostic: {err}");
534 }
535
536 #[test]
537 fn csr_graph_rejects_mismatched_masks() {
538 let err = CsrGraph::new(2, &[0, 1, 1], &[1], &[])
539 .validate("shared graph layout test")
540 .expect_err("mask length must match final CSR edge count");
541 assert!(
542 err.contains("edge_kind_mask"),
543 "unexpected diagnostic: {err}"
544 );
545 }
546
547 #[test]
548 fn csr_graph_normalizes_rows_for_shared_cache_keys() {
549 let graph = CsrGraph::new(
550 3,
551 &[0, 3, 4, 4],
552 &[2, 1, 1, 2],
553 &[
554 vyre_primitives::predicate::edge_kind::CONTROL,
555 vyre_primitives::predicate::edge_kind::ASSIGNMENT,
556 vyre_primitives::predicate::edge_kind::CALL_ARG,
557 vyre_primitives::predicate::edge_kind::CONTROL,
558 ],
559 )
560 .normalize("shared graph layout normalize test")
561 .expect("valid graph must normalize");
562
563 assert_eq!(graph.node_count(), 3);
564 assert_eq!(graph.edge_count(), 3);
565 assert_eq!(graph.edge_offsets(), &[0, 2, 3, 3]);
566 assert_eq!(graph.edge_targets(), &[1, 2, 2]);
567 assert_eq!(
568 graph.edge_kind_mask(),
569 &[
570 vyre_primitives::predicate::edge_kind::ASSIGNMENT
571 | vyre_primitives::predicate::edge_kind::CALL_ARG,
572 vyre_primitives::predicate::edge_kind::CONTROL,
573 vyre_primitives::predicate::edge_kind::CONTROL,
574 ]
575 );
576 }
577
578 #[test]
579 fn csr_graph_singleton_rows_normalize_without_sort_scratch_growth() {
580 let mut normalized = NormalizedCsrGraph::empty();
581 let mut scratch = CsrGraphNormalizationScratch::new();
582 CsrGraph::new(
583 5,
584 &[0, 1, 1, 2, 2, 3],
585 &[3, 4, 1],
586 &[
587 vyre_primitives::predicate::edge_kind::CONTROL,
588 vyre_primitives::predicate::edge_kind::ASSIGNMENT,
589 vyre_primitives::predicate::edge_kind::CALL_ARG,
590 ],
591 )
592 .normalize_into(
593 "shared graph layout singleton-row fast path test",
594 &mut normalized,
595 &mut scratch,
596 )
597 .expect("singleton-heavy graph must normalize");
598
599 assert_eq!(normalized.edge_offsets(), &[0, 1, 1, 2, 2, 3]);
600 assert_eq!(normalized.edge_targets(), &[3, 4, 1]);
601 assert_eq!(
602 normalized.edge_kind_mask(),
603 &[
604 vyre_primitives::predicate::edge_kind::CONTROL,
605 vyre_primitives::predicate::edge_kind::ASSIGNMENT,
606 vyre_primitives::predicate::edge_kind::CALL_ARG,
607 ]
608 );
609 assert!(
610 scratch.row_capacity() <= 1,
611 "Fix: singleton CSR rows should not grow normalization scratch for sort/dedup work"
612 );
613 }
614
615 #[test]
616 fn csr_graph_sorted_unique_multi_rows_bypass_sort_scratch() {
617 let mut normalized = NormalizedCsrGraph::empty();
618 let mut scratch = CsrGraphNormalizationScratch::new();
619 CsrGraph::new(
620 5,
621 &[0, 3, 5, 5, 5, 5],
622 &[1, 2, 4, 0, 2],
623 &[
624 vyre_primitives::predicate::edge_kind::CONTROL,
625 vyre_primitives::predicate::edge_kind::ASSIGNMENT,
626 vyre_primitives::predicate::edge_kind::CALL_ARG,
627 vyre_primitives::predicate::edge_kind::CONTROL,
628 vyre_primitives::predicate::edge_kind::ASSIGNMENT,
629 ],
630 )
631 .normalize_into(
632 "shared graph layout sorted-row fast path test",
633 &mut normalized,
634 &mut scratch,
635 )
636 .expect("sorted unique rows must normalize without scratch sorting");
637
638 assert_eq!(normalized.edge_offsets(), &[0, 3, 5, 5, 5, 5]);
639 assert_eq!(normalized.edge_targets(), &[1, 2, 4, 0, 2]);
640 assert_eq!(
641 scratch.row_capacity(),
642 0,
643 "Fix: already-canonical multi-edge rows must append directly without row scratch growth."
644 );
645 }
646
647 #[test]
648 fn csr_graph_normalizes_into_caller_owned_buffers_without_reallocating() {
649 let mut normalized = NormalizedCsrGraph::empty();
650 let mut scratch = CsrGraphNormalizationScratch::with_row_capacity(8);
651 CsrGraph::new(
652 3,
653 &[0, 3, 4, 4],
654 &[2, 1, 1, 2],
655 &[
656 vyre_primitives::predicate::edge_kind::CONTROL,
657 vyre_primitives::predicate::edge_kind::ASSIGNMENT,
658 vyre_primitives::predicate::edge_kind::CALL_ARG,
659 vyre_primitives::predicate::edge_kind::CONTROL,
660 ],
661 )
662 .normalize_into(
663 "shared graph layout normalize into scratch test",
664 &mut normalized,
665 &mut scratch,
666 )
667 .expect("first graph must normalize into reusable buffers");
668 let offsets_capacity = normalized.edge_offsets.capacity();
669 let targets_capacity = normalized.edge_targets.capacity();
670 let masks_capacity = normalized.edge_kind_mask.capacity();
671 let row_capacity = scratch.row_capacity();
672 let first_hash = normalized.stable_layout_hash();
673
674 CsrGraph::new(
675 3,
676 &[0, 2, 3, 3],
677 &[1, 2, 2],
678 &[
679 vyre_primitives::predicate::edge_kind::ASSIGNMENT
680 | vyre_primitives::predicate::edge_kind::CALL_ARG,
681 vyre_primitives::predicate::edge_kind::CONTROL,
682 vyre_primitives::predicate::edge_kind::CONTROL,
683 ],
684 )
685 .normalize_into(
686 "shared graph layout normalize into scratch test",
687 &mut normalized,
688 &mut scratch,
689 )
690 .expect("equivalent canonical graph must reuse normalization buffers");
691
692 assert_eq!(normalized.stable_layout_hash(), first_hash);
693 assert_eq!(normalized.edge_offsets.capacity(), offsets_capacity);
694 assert_eq!(normalized.edge_targets.capacity(), targets_capacity);
695 assert_eq!(normalized.edge_kind_mask.capacity(), masks_capacity);
696 assert_eq!(scratch.row_capacity(), row_capacity);
697 }
698
699 #[test]
700 fn csr_graph_masks_edge_kinds_during_normalization_without_prefilter() {
701 let graph = CsrGraph::new(
702 2,
703 &[0, 2, 2],
704 &[1, 1],
705 &[
706 vyre_primitives::predicate::edge_kind::CONTROL
707 | vyre_primitives::predicate::edge_kind::ASSIGNMENT,
708 vyre_primitives::predicate::edge_kind::DOMINANCE,
709 ],
710 )
711 .normalize_with_edge_kind_mask(
712 "shared graph layout masked normalize test",
713 vyre_primitives::predicate::edge_kind::ASSIGNMENT,
714 )
715 .expect("valid graph must normalize with masked edge kinds");
716
717 assert_eq!(graph.edge_targets(), &[1]);
718 assert_eq!(
719 graph.edge_kind_mask(),
720 &[vyre_primitives::predicate::edge_kind::ASSIGNMENT]
721 );
722 }
723
724 #[test]
725 fn csr_graph_preserves_zero_mask_singletons_during_normalization() {
726 let graph = CsrGraph::new(2, &[0, 2, 2], &[1, 1], &[0, 0])
727 .normalize("shared graph layout zero-mask singleton normalize test")
728 .expect("valid graph must normalize zero-mask singleton edges");
729
730 assert_eq!(graph.edge_count(), 1);
731 assert_eq!(graph.edge_offsets(), &[0, 1, 1]);
732 assert_eq!(graph.edge_targets(), &[1]);
733 assert_eq!(graph.edge_kind_mask(), &[0]);
734 }
735
736 #[test]
737 fn masked_normalization_drops_edges_that_cannot_fire() {
738 let graph = CsrGraph::new(
739 3,
740 &[0, 2, 3, 3],
741 &[1, 2, 2],
742 &[
743 vyre_primitives::predicate::edge_kind::CONTROL,
744 vyre_primitives::predicate::edge_kind::ASSIGNMENT,
745 vyre_primitives::predicate::edge_kind::CONTROL,
746 ],
747 )
748 .normalize_with_edge_kind_mask(
749 "shared graph layout masked normalize test",
750 vyre_primitives::predicate::edge_kind::ASSIGNMENT,
751 )
752 .expect("valid graph must normalize under mask");
753
754 assert_eq!(graph.edge_count(), 1);
755 assert_eq!(graph.edge_offsets(), &[0, 1, 1, 1]);
756 assert_eq!(graph.edge_targets(), &[2]);
757 assert_eq!(
758 graph.edge_kind_mask(),
759 &[vyre_primitives::predicate::edge_kind::ASSIGNMENT]
760 );
761 }
762
763 #[test]
764 fn csr_graph_rejects_zero_node_graph_with_non_empty_edges() {
765 let err = CsrGraph::new(0, &[1], &[0], &[0])
766 .validate("test")
767 .expect_err("zero-node with edges");
768 assert!(err.contains("edge_offsets[0]"), "{err}");
769 }
770
771 #[test]
772 fn csr_graph_rejects_nonzero_first_offset() {
773 let err = CsrGraph::new(2, &[1, 1, 1], &[0], &[0])
774 .validate("test")
775 .expect_err("nonzero first offset");
776 assert!(err.contains("edge_offsets[0]"), "{err}");
777 }
778
779 #[test]
780 fn csr_graph_rejects_missing_sentinel_offset() {
781 let err = CsrGraph::new(2, &[0, 1], &[0], &[0])
782 .validate("test")
783 .expect_err("missing sentinel");
784 assert!(err.contains("expected exactly node_count + 1"), "{err}");
785 }
786
787 #[test]
788 fn csr_graph_rejects_target_outside_domain() {
789 let err = CsrGraph::new(2, &[0, 1, 1], &[2], &[0])
790 .validate("test")
791 .expect_err("OOB target");
792 assert!(err.contains("targets node 2"), "{err}");
793 }
794
795 #[test]
796 fn csr_graph_rejects_mismatched_edge_kind_mask() {
797 let err = CsrGraph::new(2, &[0, 1, 1], &[0], &[0, 0])
798 .validate("test")
799 .expect_err("mask mismatch");
800 assert!(err.contains("edge_kind_mask"), "{err}");
801 }
802
803 #[test]
804 fn normalize_rejects_non_monotonic_offsets() {
805 let err = CsrGraph::new(3, &[0, 1, 0, 1], &[0], &[0])
806 .normalize("test")
807 .expect_err("non-monotonic");
808 assert!(err.contains("not monotonic"), "{err}");
809 }
810
811 #[test]
812 fn normalize_rejects_mismatched_edge_target_count() {
813 let err = CsrGraph::new(2, &[0, 1, 2], &[0], &[0])
814 .normalize("test")
815 .expect_err("mismatched edge target count");
816 assert!(err.contains("edge_targets has 1"), "{err}");
817 }
818
819 #[test]
820 fn linear_domain_rejects_scalar_slot_overflow_large() {
821 let err = LinearDomain::new(u32::MAX)
822 .scalar_slots("test", 2)
823 .expect_err("overflow");
824 assert!(err.contains("overflows"), "{err}");
825 }
826
827 #[test]
828 fn linear_domain_scalar_slots_returns_one_for_zero() {
829 assert_eq!(LinearDomain::new(0).scalar_slots("test", 2).unwrap(), 1);
830 }
831}