1use std::sync::Arc;
8
9use super::padded_u32_slice_fingerprint as motif_padded_slice_fingerprint;
10use vyre_foundation::ir::model::expr::Ident;
11use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
12
13use crate::graph::program_graph::{
14 ProgramGraphShape, BINDING_PRIMITIVE_START, NAME_EDGE_KIND_MASK, NAME_EDGE_OFFSETS,
15 NAME_EDGE_TARGETS,
16};
17
18pub const OP_ID: &str = "vyre-primitives::graph::motif";
20pub const MOTIF_HITS_BUFFER: u32 = BINDING_PRIMITIVE_START;
22pub const MOTIF_WITNESS_OUT_BUFFER: u32 = BINDING_PRIMITIVE_START + 1;
24pub const MOTIF_WORKGROUP_SIZE: [u32; 3] = [1, 1, 1];
26pub const MOTIF_DISPATCH_GRID: [u32; 3] = [1, 1, 1];
28
29#[derive(Clone, Copy, Debug, Eq, PartialEq)]
31pub struct MotifLayout {
32 pub node_count: u32,
34 pub output_words: usize,
36 pub edge_count: u32,
38 pub edge_storage_words: usize,
40 pub motif_edge_count: u32,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub struct MotifEdge {
47 pub from: u32,
49 pub kind_mask: u32,
51 pub to: u32,
53}
54
55#[derive(Clone, Debug, Eq, PartialEq)]
57pub struct MotifProgramCacheKey {
58 pub node_count: u32,
60 pub edge_count: u32,
62 pub motif_edges: Vec<MotifEdge>,
64 pub witness_out: String,
66}
67
68#[derive(Clone, Copy, Debug, Eq, PartialEq)]
75pub struct MotifStaticInputKey {
76 pub node_count: u32,
78 pub output_words: usize,
80 pub edge_storage_words: usize,
82 pub edge_offsets_hash: u64,
84 pub edge_targets_hash: u64,
86 pub edge_kind_mask_hash: u64,
88}
89
90pub struct MotifLaunchPlan {
92 layout: MotifLayout,
93 cache_key: MotifProgramCacheKey,
94}
95
96impl MotifLaunchPlan {
97 #[must_use]
99 pub const fn layout(&self) -> MotifLayout {
100 self.layout
101 }
102
103 #[must_use]
105 pub fn cache_key(&self) -> &MotifProgramCacheKey {
106 &self.cache_key
107 }
108
109 #[must_use]
111 pub const fn output_words(&self) -> usize {
112 self.layout.output_words
113 }
114
115 #[must_use]
117 pub const fn edge_storage_words(&self) -> usize {
118 self.layout.edge_storage_words
119 }
120
121 #[must_use]
123 pub const fn dispatch_grid(&self) -> [u32; 3] {
124 MOTIF_DISPATCH_GRID
125 }
126
127 #[must_use]
129 pub fn program(&self) -> Program {
130 motif(
131 ProgramGraphShape::new(self.layout.node_count, self.layout.edge_count.max(1)),
132 &self.cache_key.motif_edges,
133 &self.cache_key.witness_out,
134 )
135 }
136
137 pub fn static_input_key(
144 &self,
145 edge_offsets: &[u32],
146 edge_targets: &[u32],
147 edge_kind_mask: &[u32],
148 ) -> Result<MotifStaticInputKey, String> {
149 if edge_offsets.len() != self.layout.node_count as usize + 1 {
150 return Err(format!(
151 "Fix: motif static key expected {} offset words, got {}.",
152 self.layout.node_count as usize + 1,
153 edge_offsets.len()
154 ));
155 }
156 if edge_targets.len() != self.layout.edge_count as usize {
157 return Err(format!(
158 "Fix: motif static key expected {} target word(s), got {}.",
159 self.layout.edge_count,
160 edge_targets.len()
161 ));
162 }
163 if edge_kind_mask.len() != self.layout.edge_count as usize {
164 return Err(format!(
165 "Fix: motif static key expected {} kind-mask word(s), got {}.",
166 self.layout.edge_count,
167 edge_kind_mask.len()
168 ));
169 }
170 Ok(MotifStaticInputKey {
171 node_count: self.layout.node_count,
172 output_words: self.layout.output_words,
173 edge_storage_words: self.layout.edge_storage_words,
174 edge_offsets_hash: motif_padded_slice_fingerprint(edge_offsets, edge_offsets.len()),
175 edge_targets_hash: motif_padded_slice_fingerprint(
176 edge_targets,
177 self.layout.edge_storage_words,
178 ),
179 edge_kind_mask_hash: motif_padded_slice_fingerprint(
180 edge_kind_mask,
181 self.layout.edge_storage_words,
182 ),
183 })
184 }
185}
186
187pub struct MotifDispatchPlan {
189 layout: MotifLayout,
190 program: Program,
191}
192
193impl MotifDispatchPlan {
194 #[must_use]
196 pub const fn layout(&self) -> MotifLayout {
197 self.layout
198 }
199
200 #[must_use]
202 pub const fn program(&self) -> &Program {
203 &self.program
204 }
205
206 #[must_use]
208 pub const fn output_words(&self) -> usize {
209 self.layout.output_words
210 }
211
212 #[must_use]
214 pub const fn edge_storage_words(&self) -> usize {
215 self.layout.edge_storage_words
216 }
217
218 #[must_use]
220 pub const fn dispatch_grid(&self) -> [u32; 3] {
221 MOTIF_DISPATCH_GRID
222 }
223}
224
225pub fn plan_motif_launch(
233 node_count: u32,
234 edge_offsets: &[u32],
235 edge_targets: &[u32],
236 edge_kind_mask: &[u32],
237 motif_edges: &[MotifEdge],
238 witness_out: &str,
239) -> Result<MotifLaunchPlan, String> {
240 let layout = validate_motif_inputs(
241 node_count,
242 edge_offsets,
243 edge_targets,
244 edge_kind_mask,
245 motif_edges,
246 )?;
247
248 Ok(MotifLaunchPlan {
249 layout,
250 cache_key: MotifProgramCacheKey {
251 node_count: layout.node_count,
252 edge_count: layout.edge_count,
253 motif_edges: motif_edges.to_vec(),
254 witness_out: witness_out.to_string(),
255 },
256 })
257}
258
259pub fn plan_motif_dispatch(
267 node_count: u32,
268 edge_offsets: &[u32],
269 edge_targets: &[u32],
270 edge_kind_mask: &[u32],
271 motif_edges: &[MotifEdge],
272 witness_out: &str,
273) -> Result<MotifDispatchPlan, String> {
274 let launch = plan_motif_launch(
275 node_count,
276 edge_offsets,
277 edge_targets,
278 edge_kind_mask,
279 motif_edges,
280 witness_out,
281 )?;
282 let layout = launch.layout();
283 let program = launch.program();
284 Ok(MotifDispatchPlan { layout, program })
285}
286
287#[must_use]
295pub fn motif(shape: ProgramGraphShape, edges: &[MotifEdge], witness_out: &str) -> Program {
296 let Ok(edge_count) = u32::try_from(edges.len()) else {
297 return crate::invalid_output_program(
298 OP_ID,
299 witness_out,
300 DataType::U32,
301 "Fix: motif edges.len() exceeds u32::MAX; split the motif or redesign the caller."
302 .to_string(),
303 );
304 };
305 let mut buffers = shape.read_only_buffers();
306 buffers.push(
307 BufferDecl::storage(
308 "motif_hits",
309 MOTIF_HITS_BUFFER,
310 BufferAccess::ReadWrite,
311 DataType::U32,
312 )
313 .with_count(shape.node_count.max(1)),
314 );
315 buffers.push(
316 BufferDecl::storage(
317 witness_out,
318 MOTIF_WITNESS_OUT_BUFFER,
319 BufferAccess::ReadWrite,
320 DataType::U32,
321 )
322 .with_count(shape.node_count.max(1)),
323 );
324
325 let clear_outputs = vec![
326 Node::store("motif_hits", Expr::var("node"), Expr::u32(0)),
327 Node::store(witness_out, Expr::var("node"), Expr::u32(0)),
328 ];
329 let Some(scan_capacity) = edges.len().checked_mul(5) else {
334 return crate::invalid_output_program(
335 OP_ID,
336 witness_out,
337 DataType::U32,
338 "Fix: motif scan node count overflows usize; split the motif before lowering."
339 .to_string(),
340 );
341 };
342 let Some(mark_capacity) = edges.len().checked_mul(2) else {
343 return crate::invalid_output_program(
344 OP_ID,
345 witness_out,
346 DataType::U32,
347 "Fix: motif witness mark count overflows usize; split the motif before lowering."
348 .to_string(),
349 );
350 };
351 let mut scan_edges: Vec<Node> = Vec::new();
352 if let Err(error) = scan_edges.try_reserve(scan_capacity) {
353 return crate::invalid_output_program(
354 OP_ID,
355 witness_out,
356 DataType::U32,
357 format!("Fix: motif lowering could not reserve {scan_capacity} scan nodes: {error}"),
358 );
359 }
360 let mut mark_hits: Vec<Node> = Vec::new();
361 if let Err(error) = mark_hits.try_reserve(mark_capacity) {
362 return crate::invalid_output_program(
363 OP_ID,
364 witness_out,
365 DataType::U32,
366 format!("Fix: motif lowering could not reserve {mark_capacity} mark nodes: {error}"),
367 );
368 }
369 for (idx, edge) in edges.iter().enumerate() {
370 let edge_found = format!("edge_found_{idx}");
371 let edge_start = format!("edge_start_{idx}");
372 let edge_end = format!("edge_end_{idx}");
373 let edge_index = format!("e_{idx}");
374 let actual_dst = format!("actual_dst_{idx}");
375 let actual_kind = format!("actual_kind_{idx}");
376 scan_edges.push(Node::let_bind(&edge_found, Expr::u32(0)));
377 if edge.from < shape.node_count {
378 scan_edges.push(Node::let_bind(
379 &edge_start,
380 Expr::load(NAME_EDGE_OFFSETS, Expr::u32(edge.from)),
381 ));
382 scan_edges.push(Node::let_bind(
383 &edge_end,
384 Expr::load(NAME_EDGE_OFFSETS, Expr::u32(edge.from.saturating_add(1))),
385 ));
386 scan_edges.push(Node::loop_for(
387 &edge_index,
388 Expr::var(&edge_start),
389 Expr::var(&edge_end),
390 vec![
391 Node::let_bind(
392 &actual_dst,
393 Expr::load(NAME_EDGE_TARGETS, Expr::var(&edge_index)),
394 ),
395 Node::let_bind(
396 &actual_kind,
397 Expr::load(NAME_EDGE_KIND_MASK, Expr::var(&edge_index)),
398 ),
399 Node::if_then(
400 Expr::and(
401 Expr::eq(Expr::var(&actual_dst), Expr::u32(edge.to)),
402 Expr::ne(
403 Expr::bitand(Expr::var(&actual_kind), Expr::u32(edge.kind_mask)),
404 Expr::u32(0),
405 ),
406 ),
407 vec![Node::assign(&edge_found, Expr::u32(1))],
408 ),
409 ],
410 ));
411 }
412 scan_edges.push(Node::if_then(
413 Expr::ne(Expr::var(&edge_found), Expr::u32(0)),
414 vec![Node::assign(
415 "matched_edges",
416 Expr::add(Expr::var("matched_edges"), Expr::u32(1)),
417 )],
418 ));
419 if edge.from < shape.node_count {
420 mark_hits.push(Node::store(
421 "motif_hits",
422 Expr::u32(edge.from),
423 Expr::u32(1),
424 ));
425 }
426 if edge.to < shape.node_count {
427 mark_hits.push(Node::store("motif_hits", Expr::u32(edge.to), Expr::u32(1)));
428 }
429 }
430 let materialize = vec![Node::store(
431 witness_out,
432 Expr::var("node"),
433 Expr::load("motif_hits", Expr::var("node")),
434 )];
435 let mut publish_full_match = mark_hits;
436 publish_full_match.push(Node::loop_for(
437 "node",
438 Expr::u32(0),
439 Expr::u32(shape.node_count),
440 materialize,
441 ));
442
443 Program::wrapped(
450 buffers,
451 MOTIF_WORKGROUP_SIZE,
452 vec![Node::Region {
453 generator: Ident::from(OP_ID),
454 source_region: None,
455 body: Arc::new(vec![
456 Node::loop_for(
457 "node",
458 Expr::u32(0),
459 Expr::u32(shape.node_count),
460 clear_outputs,
461 ),
462 Node::let_bind("matched_edges", Expr::u32(0)),
463 Node::Block(scan_edges),
464 Node::if_then(
465 Expr::eq(Expr::var("matched_edges"), Expr::u32(edge_count)),
466 publish_full_match,
467 ),
468 ]),
469 }],
470 )
471}
472
473#[must_use]
476#[cfg(any(test, feature = "cpu-parity"))]
477pub fn cpu_ref(
478 node_count: u32,
479 edge_offsets: &[u32],
480 edge_targets: &[u32],
481 edge_kind_mask: &[u32],
482 motif_edges: &[MotifEdge],
483) -> Vec<u32> {
484 let mut participants = Vec::new();
485 try_cpu_ref_into(
486 node_count,
487 edge_offsets,
488 edge_targets,
489 edge_kind_mask,
490 motif_edges,
491 &mut participants,
492 )
493 .unwrap_or_else(|err| panic!("motif CPU oracle received malformed input. {err}"));
494 participants
495}
496
497#[cfg(any(test, feature = "cpu-parity"))]
499pub fn try_cpu_ref_into(
500 node_count: u32,
501 edge_offsets: &[u32],
502 edge_targets: &[u32],
503 edge_kind_mask: &[u32],
504 motif_edges: &[MotifEdge],
505 participants: &mut Vec<u32>,
506) -> Result<(), String> {
507 let layout = validate_motif_inputs(
508 node_count,
509 edge_offsets,
510 edge_targets,
511 edge_kind_mask,
512 motif_edges,
513 )?;
514 crate::graph::scratch::reserve_graph_items(
515 participants,
516 layout.output_words,
517 "motif CPU oracle",
518 "motif witness output",
519 )?;
520 participants.clear();
521 participants.resize(layout.output_words, 0);
522 if !motif_all_edges_present(edge_offsets, edge_targets, edge_kind_mask, motif_edges) {
523 return Ok(());
524 }
525 for motif_edge in motif_edges {
526 if let Some(hit) = participants.get_mut(motif_edge.from as usize) {
527 *hit = 1;
528 }
529 if let Some(hit) = participants.get_mut(motif_edge.to as usize) {
530 *hit = 1;
531 }
532 }
533 Ok(())
534}
535
536#[cfg(any(test, feature = "cpu-parity"))]
538pub fn cpu_ref_into(
539 node_count: u32,
540 edge_offsets: &[u32],
541 edge_targets: &[u32],
542 edge_kind_mask: &[u32],
543 motif_edges: &[MotifEdge],
544 participants: &mut Vec<u32>,
545) {
546 try_cpu_ref_into(
547 node_count,
548 edge_offsets,
549 edge_targets,
550 edge_kind_mask,
551 motif_edges,
552 participants,
553 )
554 .unwrap_or_else(|err| panic!("motif CPU oracle received malformed input. {err}"));
555}
556
557#[must_use]
561#[cfg(any(test, feature = "cpu-parity"))]
562pub fn cpu_ref_matches(
563 edge_offsets: &[u32],
564 edge_targets: &[u32],
565 edge_kind_mask: &[u32],
566 motif_edges: &[MotifEdge],
567) -> bool {
568 motif_all_edges_present(edge_offsets, edge_targets, edge_kind_mask, motif_edges)
569}
570
571#[must_use]
576#[cfg(any(test, feature = "cpu-parity"))]
577pub fn cpu_ref_participation_count(
578 node_count: u32,
579 edge_offsets: &[u32],
580 edge_targets: &[u32],
581 edge_kind_mask: &[u32],
582 motif_edges: &[MotifEdge],
583) -> u32 {
584 try_cpu_ref_participation_count(
585 node_count,
586 edge_offsets,
587 edge_targets,
588 edge_kind_mask,
589 motif_edges,
590 )
591 .unwrap_or_else(|err| panic!("motif participation oracle received malformed input. {err}"))
592}
593
594#[cfg(any(test, feature = "cpu-parity"))]
596#[derive(Debug, Default, Clone)]
597pub struct MotifCpuScratch {
598 pub endpoints: Vec<u32>,
600}
601
602#[cfg(any(test, feature = "cpu-parity"))]
603impl MotifCpuScratch {
604 pub fn new() -> Self {
606 Self::default()
607 }
608}
609
610#[cfg(any(test, feature = "cpu-parity"))]
612pub fn try_cpu_ref_participation_count(
613 node_count: u32,
614 edge_offsets: &[u32],
615 edge_targets: &[u32],
616 edge_kind_mask: &[u32],
617 motif_edges: &[MotifEdge],
618) -> Result<u32, String> {
619 let mut scratch = MotifCpuScratch::default();
620 try_cpu_ref_participation_count_with_scratch(
621 node_count,
622 edge_offsets,
623 edge_targets,
624 edge_kind_mask,
625 motif_edges,
626 &mut scratch,
627 )
628}
629
630#[cfg(any(test, feature = "cpu-parity"))]
636pub fn try_cpu_ref_participation_count_with_scratch(
637 node_count: u32,
638 edge_offsets: &[u32],
639 edge_targets: &[u32],
640 edge_kind_mask: &[u32],
641 motif_edges: &[MotifEdge],
642 scratch: &mut MotifCpuScratch,
643) -> Result<u32, String> {
644 validate_motif_inputs(
645 node_count,
646 edge_offsets,
647 edge_targets,
648 edge_kind_mask,
649 motif_edges,
650 )?;
651 let endpoint_count = motif_edges
652 .len()
653 .checked_mul(2)
654 .ok_or_else(|| "Fix: motif endpoint count overflows usize.".to_string())?;
655 scratch
656 .endpoints
657 .try_reserve(endpoint_count)
658 .map_err(|error| {
659 format!(
660 "Fix: motif participation oracle could not reserve {endpoint_count} endpoints: {error}"
661 )
662 })?;
663 scratch.endpoints.clear();
664 if !motif_all_edges_present(edge_offsets, edge_targets, edge_kind_mask, motif_edges) {
665 return Ok(0);
666 }
667 for motif_edge in motif_edges {
668 if motif_edge.from < node_count {
669 scratch.endpoints.push(motif_edge.from);
670 }
671 if motif_edge.to < node_count {
672 scratch.endpoints.push(motif_edge.to);
673 }
674 }
675 scratch.endpoints.sort_unstable();
676 scratch.endpoints.dedup();
677 u32::try_from(scratch.endpoints.len()).map_err(|error| {
678 format!("Fix: motif participation count does not fit u32 after deduplication: {error}")
679 })
680}
681
682pub fn validate_csr_inputs(
693 node_count: u32,
694 edge_offsets: &[u32],
695 edge_targets: &[u32],
696 edge_kind_mask: &[u32],
697) -> Result<MotifLayout, String> {
698 validate_motif_inputs(node_count, edge_offsets, edge_targets, edge_kind_mask, &[])
699}
700
701pub fn validate_motif_inputs(
709 node_count: u32,
710 edge_offsets: &[u32],
711 edge_targets: &[u32],
712 edge_kind_mask: &[u32],
713 motif_edges: &[MotifEdge],
714) -> Result<MotifLayout, String> {
715 let expected_offsets = (node_count as usize).checked_add(1).ok_or_else(|| {
716 format!("Fix: motif node_count + 1 overflows usize for node_count={node_count}.")
717 })?;
718 if edge_offsets.len() != expected_offsets {
719 return Err(format!(
720 "Fix: motif requires edge_offsets.len() == node_count + 1, got len={}, node_count={node_count}.",
721 edge_offsets.len()
722 ));
723 }
724 if edge_targets.len() != edge_kind_mask.len() {
725 return Err(format!(
726 "Fix: motif requires edge_targets.len() == edge_kind_mask.len(), got {} vs {}.",
727 edge_targets.len(),
728 edge_kind_mask.len()
729 ));
730 }
731 if let Some(&first) = edge_offsets.first() {
732 if first != 0 {
733 return Err(format!(
734 "Fix: motif requires edge_offsets[0] == 0, got {first}."
735 ));
736 }
737 }
738 for (index, pair) in edge_offsets.windows(2).enumerate() {
739 if pair[0] > pair[1] {
740 return Err(format!(
741 "Fix: motif offsets must be monotonic; offsets[{index}]={} > offsets[{}]={}.",
742 pair[0],
743 index + 1,
744 pair[1]
745 ));
746 }
747 }
748 let edge_count = edge_offsets[expected_offsets - 1] as usize;
749 if edge_targets.len() != edge_count {
750 return Err(format!(
751 "Fix: motif final offset declares edge_count={edge_count}, but targets_len={} and kind_mask_len={}.",
752 edge_targets.len(),
753 edge_kind_mask.len()
754 ));
755 }
756 for (index, &target) in edge_targets.iter().enumerate() {
757 if target >= node_count {
758 return Err(format!(
759 "Fix: motif edge_targets[{index}]={target} is outside node_count {node_count}."
760 ));
761 }
762 }
763 for (index, motif_edge) in motif_edges.iter().enumerate() {
764 if motif_edge.from >= node_count {
765 return Err(format!(
766 "Fix: motif_edges[{index}].from={} is outside node_count {node_count}.",
767 motif_edge.from
768 ));
769 }
770 if motif_edge.to >= node_count {
771 return Err(format!(
772 "Fix: motif_edges[{index}].to={} is outside node_count {node_count}.",
773 motif_edge.to
774 ));
775 }
776 }
777 let edge_count = u32::try_from(edge_count)
778 .map_err(|_| format!("Fix: motif edge count {edge_count} exceeds u32 index space."))?;
779 let motif_edge_count = u32::try_from(motif_edges.len()).map_err(|_| {
780 format!(
781 "Fix: motif edge pattern count {} exceeds u32 index space.",
782 motif_edges.len()
783 )
784 })?;
785 Ok(MotifLayout {
786 node_count,
787 output_words: node_count as usize,
788 edge_count,
789 edge_storage_words: edge_targets.len().max(1),
790 motif_edge_count,
791 })
792}
793
794pub fn count_witness_participants(witness: &[u32]) -> Result<u32, String> {
801 let count = witness.iter().filter(|&&value| value != 0).count();
802 u32::try_from(count)
803 .map_err(|_| format!("Fix: motif witness participant count {count} exceeds u32::MAX."))
804}
805
806pub fn validate_motif_witness(layout: MotifLayout, witness: &[u32]) -> Result<(), String> {
813 if witness.len() != layout.output_words {
814 return Err(format!(
815 "Fix: motif witness expected {} word(s), got {}.",
816 layout.output_words,
817 witness.len()
818 ));
819 }
820 for (index, &value) in witness.iter().enumerate() {
821 if value > 1 {
822 return Err(format!(
823 "Fix: motif witness[{index}]={value} is not boolean; expected 0 or 1."
824 ));
825 }
826 }
827 Ok(())
828}
829
830#[cfg(test)]
831mod dispatch_contract_tests {
832 use super::*;
833
834 #[test]
835 fn static_input_key_tracks_graph_content_not_motif_program() {
836 let first_motif = [MotifEdge {
837 from: 0,
838 kind_mask: 1,
839 to: 1,
840 }];
841 let second_motif = [MotifEdge {
842 from: 1,
843 kind_mask: 1,
844 to: 2,
845 }];
846 let first = plan_motif_launch(3, &[0, 1, 2, 2], &[1, 2], &[1, 1], &first_motif, "w")
847 .expect("Fix: first motif launch should plan");
848 let second = plan_motif_launch(3, &[0, 1, 2, 2], &[1, 2], &[1, 1], &second_motif, "w")
849 .expect("Fix: second motif launch should plan");
850
851 assert_ne!(first.cache_key(), second.cache_key());
852 assert_eq!(
853 first
854 .static_input_key(&[0, 1, 2, 2], &[1, 2], &[1, 1])
855 .expect("Fix: first motif static key should build"),
856 second
857 .static_input_key(&[0, 1, 2, 2], &[1, 2], &[1, 1])
858 .expect("Fix: second motif static key should build")
859 );
860 }
861
862 #[test]
863 fn static_input_key_refreshes_on_same_shape_graph_content_change() {
864 let motif = [MotifEdge {
865 from: 0,
866 kind_mask: 1,
867 to: 1,
868 }];
869 let plan = plan_motif_launch(3, &[0, 1, 2, 2], &[1, 2], &[1, 1], &motif, "w")
870 .expect("Fix: motif launch should plan");
871 let first = plan
872 .static_input_key(&[0, 1, 2, 2], &[1, 2], &[1, 1])
873 .expect("Fix: first static key should build");
874 let changed = plan
875 .static_input_key(&[0, 1, 2, 2], &[2, 2], &[1, 1])
876 .expect("Fix: same-shape changed graph should key");
877
878 assert_eq!(first.edge_offsets_hash, changed.edge_offsets_hash);
879 assert_eq!(first.edge_kind_mask_hash, changed.edge_kind_mask_hash);
880 assert_ne!(first.edge_targets_hash, changed.edge_targets_hash);
881 assert_ne!(first, changed);
882 }
883
884 #[test]
885 fn static_input_key_rejects_shape_drift() {
886 let motif = [MotifEdge {
887 from: 0,
888 kind_mask: 1,
889 to: 1,
890 }];
891 let plan = plan_motif_launch(2, &[0, 1, 1], &[1], &[1], &motif, "w")
892 .expect("Fix: motif launch should plan");
893 let err = plan
894 .static_input_key(&[0, 1, 1], &[], &[])
895 .expect_err("Fix: stale motif plan must reject edge-array drift");
896
897 assert!(err.contains("expected 1 target"));
898 }
899
900 #[test]
901 fn witness_validation_rejects_non_boolean_backend_output() {
902 let layout = validate_motif_inputs(3, &[0, 1, 2, 2], &[1, 2], &[1, 1], &[])
903 .expect("Fix: valid graph should validate");
904
905 validate_motif_witness(layout, &[0, 1, 0]).expect("Fix: boolean witness is valid");
906 let err = validate_motif_witness(layout, &[0, 2, 0])
907 .expect_err("Fix: non-boolean witness must be rejected");
908
909 assert!(err.contains("witness[1]=2 is not boolean"));
910 }
911}
912
913#[cfg(any(test, feature = "cpu-parity"))]
914fn motif_all_edges_present(
915 edge_offsets: &[u32],
916 edge_targets: &[u32],
917 edge_kind_mask: &[u32],
918 motif_edges: &[MotifEdge],
919) -> bool {
920 for motif_edge in motif_edges {
921 let Some(start) = edge_offsets.get(motif_edge.from as usize).copied() else {
922 return false;
923 };
924 let Some(end) = edge_offsets.get(motif_edge.from as usize + 1).copied() else {
925 return false;
926 };
927 let start = start as usize;
928 let end = end as usize;
929 let mut found = false;
930 for edge_idx in start..end {
931 let Some(dst) = edge_targets.get(edge_idx).copied() else {
932 break;
933 };
934 let Some(kind) = edge_kind_mask.get(edge_idx).copied() else {
935 break;
936 };
937 if dst == motif_edge.to && (kind & motif_edge.kind_mask) != 0 {
938 found = true;
939 break;
940 }
941 }
942 if !found {
943 return false;
944 }
945 }
946 true
947}
948
949#[cfg(feature = "inventory-registry")]
950inventory::submit! {
951 vyre_foundation::operation::OperationRegistration::primitive(
952 OP_ID,
953 || motif(ProgramGraphShape::new(4, 4), &[MotifEdge { from: 0, to: 1, kind_mask: 1 }], "witness"),
954 Some(|| {
955 let to_bytes = |w: &[u32]| crate::wire::pack_u32_slice(w);
956 vec![vec![
957 to_bytes(&[0, 0, 0, 0]), to_bytes(&[0, 2, 3, 4, 4]), to_bytes(&[1, 2, 3, 3]), to_bytes(&[1, 1, 1, 1]), to_bytes(&[0, 0, 0, 0]), to_bytes(&[0, 0, 0, 0]), to_bytes(&[0, 0, 0, 0]), ]]
965 }),
966 Some(|| {
967 let to_bytes = |w: &[u32]| crate::wire::pack_u32_slice(w);
968 vec![vec![
969 to_bytes(&[1, 1, 0, 0]), to_bytes(&[1, 1, 0, 0]), ]]
972 }),
973 )
974}
975
976#[cfg(test)]
977mod tests {
978 use super::*;
979
980 #[test]
981 fn try_cpu_ref_into_rejects_bad_motif_endpoint_without_clobbering_witness() {
982 let mut witness = vec![9, 8, 7];
983 let motif = [MotifEdge {
984 from: 0,
985 kind_mask: 1,
986 to: 3,
987 }];
988
989 let err = try_cpu_ref_into(3, &[0, 1, 1, 1], &[1], &[1], &motif, &mut witness)
990 .expect_err("motif endpoint beyond node_count must fail validation");
991
992 assert!(
993 err.contains("motif_edges[0].to=3 is outside node_count 3"),
994 "Fix: motif endpoint errors must identify the bad endpoint, got: {err}"
995 );
996 assert_eq!(
997 witness,
998 vec![9, 8, 7],
999 "failed motif preflight must preserve the previous witness vector"
1000 );
1001 }
1002
1003 #[test]
1004 fn try_participation_count_rejects_bad_motif_endpoint() {
1005 let motif = [MotifEdge {
1006 from: 4,
1007 kind_mask: 1,
1008 to: 0,
1009 }];
1010
1011 let err = try_cpu_ref_participation_count(3, &[0, 0, 0, 0], &[], &[], &motif)
1012 .expect_err("motif participation count must validate pattern endpoints");
1013
1014 assert!(
1015 err.contains("motif_edges[0].from=4 is outside node_count 3"),
1016 "Fix: motif participation count must surface endpoint shape errors, got: {err}"
1017 );
1018 }
1019
1020 #[test]
1021 fn try_participation_count_with_scratch_reuses_endpoint_storage() {
1022 let mut endpoints = Vec::with_capacity(8);
1023 endpoints.extend_from_slice(&[99, 98, 97]);
1024 let mut scratch = MotifCpuScratch { endpoints };
1025 let capacity = scratch.endpoints.capacity();
1026 let motif = [
1027 MotifEdge {
1028 from: 0,
1029 kind_mask: 1,
1030 to: 1,
1031 },
1032 MotifEdge {
1033 from: 1,
1034 kind_mask: 1,
1035 to: 2,
1036 },
1037 ];
1038
1039 let count = try_cpu_ref_participation_count_with_scratch(
1040 3,
1041 &[0, 1, 2, 2],
1042 &[1, 2],
1043 &[1, 1],
1044 &motif,
1045 &mut scratch,
1046 )
1047 .expect("Fix: valid motif count must run with reusable endpoint scratch.");
1048
1049 assert_eq!(count, 3);
1050 assert_eq!(scratch.endpoints.capacity(), capacity);
1051 assert_eq!(
1052 scratch.endpoints,
1053 vec![0, 1, 2],
1054 "Fix: endpoint scratch must be sorted and deduplicated for the live motif."
1055 );
1056
1057 let count = try_cpu_ref_participation_count_with_scratch(
1058 3,
1059 &[0, 1, 1, 1],
1060 &[1],
1061 &[1],
1062 &motif,
1063 &mut scratch,
1064 )
1065 .expect("Fix: valid graph with missing motif must return zero without stale endpoints.");
1066
1067 assert_eq!(count, 0);
1068 assert_eq!(scratch.endpoints.capacity(), capacity);
1069 assert!(
1070 scratch.endpoints.is_empty(),
1071 "Fix: missing motif must clear stale endpoint scratch."
1072 );
1073 }
1074
1075 #[test]
1076 fn try_participation_count_with_scratch_validates_before_mutating_storage() {
1077 let mut scratch = MotifCpuScratch {
1078 endpoints: vec![0xCAFE_BABE, 0xDEAD_BEEF],
1079 };
1080 let motif = [MotifEdge {
1081 from: 4,
1082 kind_mask: 1,
1083 to: 0,
1084 }];
1085
1086 let err = try_cpu_ref_participation_count_with_scratch(
1087 3,
1088 &[0, 0, 0, 0],
1089 &[],
1090 &[],
1091 &motif,
1092 &mut scratch,
1093 )
1094 .expect_err("Fix: motif endpoint validation must run before scratch reuse.");
1095
1096 assert!(
1097 err.contains("motif_edges[0].from=4 is outside node_count 3"),
1098 "Fix: motif participation count must surface endpoint shape errors, got: {err}"
1099 );
1100 assert_eq!(
1101 scratch.endpoints,
1102 vec![0xCAFE_BABE, 0xDEAD_BEEF],
1103 "Fix: validation failure must not clear reusable endpoint scratch."
1104 );
1105 }
1106
1107 #[test]
1108 fn generated_participation_count_matches_witness_count() {
1109 for node_count in 2u32..=7 {
1110 let mut offsets = Vec::with_capacity(node_count as usize + 1);
1111 let mut targets = Vec::new();
1112 let mut masks = Vec::new();
1113 offsets.push(0);
1114 for node in 0..node_count {
1115 targets.push((node + 1) % node_count);
1116 masks.push(1);
1117 offsets.push(targets.len() as u32);
1118 }
1119 let motif = [MotifEdge {
1120 from: 0,
1121 kind_mask: 1,
1122 to: 1,
1123 }];
1124 let witness = cpu_ref(node_count, &offsets, &targets, &masks, &motif);
1125 let witness_count =
1126 count_witness_participants(&witness).expect("Fix: replace expect with fallible API or document caller precondition; panic only on programmer error - generated witness count must fit u32");
1127 let count =
1128 try_cpu_ref_participation_count(node_count, &offsets, &targets, &masks, &motif)
1129 .expect("Fix: replace expect with fallible API or document caller precondition; panic only on programmer error - generated motif participation count must pass validation");
1130
1131 assert_eq!(
1132 count, witness_count,
1133 "participation count diverged from witness count at node_count={node_count}"
1134 );
1135 }
1136 }
1137
1138 #[test]
1139 fn three_node_chain_motif_marks_every_participant() {
1140 let witness = cpu_ref(
1141 3,
1142 &[0, 1, 2, 2],
1143 &[1, 2],
1144 &[1, 1],
1145 &[
1146 MotifEdge {
1147 from: 0,
1148 kind_mask: 1,
1149 to: 1,
1150 },
1151 MotifEdge {
1152 from: 1,
1153 kind_mask: 1,
1154 to: 2,
1155 },
1156 ],
1157 );
1158 assert_eq!(witness, vec![1, 1, 1]);
1159 }
1160
1161 #[test]
1162 fn missing_motif_edge_clears_all_participants() {
1163 let witness = cpu_ref(
1164 3,
1165 &[0, 1, 1, 1],
1166 &[1],
1167 &[1],
1168 &[
1169 MotifEdge {
1170 from: 0,
1171 kind_mask: 1,
1172 to: 1,
1173 },
1174 MotifEdge {
1175 from: 1,
1176 kind_mask: 1,
1177 to: 2,
1178 },
1179 ],
1180 );
1181 assert_eq!(witness, vec![0, 0, 0]);
1182 }
1183
1184 #[test]
1185 fn cpu_ref_into_reuses_witness_storage() {
1186 let mut witness = Vec::with_capacity(8);
1187 cpu_ref_into(
1188 3,
1189 &[0, 1, 2, 2],
1190 &[1, 2],
1191 &[1, 1],
1192 &[
1193 MotifEdge {
1194 from: 0,
1195 kind_mask: 1,
1196 to: 1,
1197 },
1198 MotifEdge {
1199 from: 1,
1200 kind_mask: 1,
1201 to: 2,
1202 },
1203 ],
1204 &mut witness,
1205 );
1206 let capacity = witness.capacity();
1207 assert_eq!(witness, vec![1, 1, 1]);
1208
1209 cpu_ref_into(
1210 3,
1211 &[0, 1, 1, 1],
1212 &[1],
1213 &[1],
1214 &[MotifEdge {
1215 from: 1,
1216 kind_mask: 1,
1217 to: 2,
1218 }],
1219 &mut witness,
1220 );
1221 assert_eq!(witness.capacity(), capacity);
1222 assert_eq!(witness, vec![0, 0, 0]);
1223 }
1224
1225 #[test]
1226 fn cpu_ref_into_validates_before_clearing_witness_storage() {
1227 let mut witness = vec![0xCAFE_BABEu32, 0xDEAD_BEEF];
1228 let ptr = witness.as_ptr();
1229 let previous_hook = std::panic::take_hook();
1230 std::panic::set_hook(Box::new(|_| {}));
1231 let err = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1232 cpu_ref_into(
1233 2,
1234 &[0, 1, 1],
1235 &[1],
1236 &[],
1237 &[MotifEdge {
1238 from: 0,
1239 kind_mask: 1,
1240 to: 1,
1241 }],
1242 &mut witness,
1243 );
1244 }));
1245 std::panic::set_hook(previous_hook);
1246
1247 assert!(err.is_err(), "mismatched CSR edge arrays must be rejected");
1248 assert_eq!(
1249 witness,
1250 vec![0xCAFE_BABEu32, 0xDEAD_BEEF],
1251 "Fix: motif CPU oracle must validate before clearing caller witness storage."
1252 );
1253 assert_eq!(witness.as_ptr(), ptr);
1254 }
1255
1256 #[test]
1257 fn generated_try_cpu_ref_into_and_count_match_witness() {
1258 for node_count in 1u32..=64 {
1259 let mut scratch = MotifCpuScratch::new();
1260 let edge_offsets: Vec<u32> = (0..=node_count).collect();
1261 let edge_targets: Vec<u32> = (0..node_count)
1262 .map(|node| (node + 1) % node_count)
1263 .collect();
1264 let edge_kind_mask = vec![1u32; node_count as usize];
1265 for motif_len in 0usize..64 {
1266 let motif_edges: Vec<MotifEdge> = (0..motif_len)
1267 .map(|index| {
1268 let from = (index as u32) % node_count;
1269 MotifEdge {
1270 from,
1271 kind_mask: 1,
1272 to: (from + 1) % node_count,
1273 }
1274 })
1275 .collect();
1276 let mut witness = vec![0xCAFE_BABEu32; 3];
1277 try_cpu_ref_into(
1278 node_count,
1279 &edge_offsets,
1280 &edge_targets,
1281 &edge_kind_mask,
1282 &motif_edges,
1283 &mut witness,
1284 )
1285 .unwrap();
1286 let count = try_cpu_ref_participation_count_with_scratch(
1287 node_count,
1288 &edge_offsets,
1289 &edge_targets,
1290 &edge_kind_mask,
1291 &motif_edges,
1292 &mut scratch,
1293 )
1294 .unwrap();
1295 assert_eq!(witness.len(), node_count as usize);
1296 assert_eq!(
1297 count,
1298 witness.iter().filter(|&&value| value != 0).count() as u32
1299 );
1300 }
1301 }
1302 }
1303
1304 #[test]
1305 fn allocation_free_predicates_match_witness_contract() {
1306 let motif = [
1307 MotifEdge {
1308 from: 0,
1309 kind_mask: 1,
1310 to: 1,
1311 },
1312 MotifEdge {
1313 from: 1,
1314 kind_mask: 1,
1315 to: 2,
1316 },
1317 ];
1318 assert!(cpu_ref_matches(&[0, 1, 2, 2], &[1, 2], &[1, 1], &motif));
1319 assert_eq!(
1320 cpu_ref_participation_count(3, &[0, 1, 2, 2], &[1, 2], &[1, 1], &motif),
1321 3
1322 );
1323 assert!(!cpu_ref_matches(&[0, 1, 1, 1], &[1], &[1], &motif));
1324 assert_eq!(
1325 cpu_ref_participation_count(3, &[0, 1, 1, 1], &[1], &[1], &motif),
1326 0
1327 );
1328 assert!(
1329 cpu_ref_matches(&[0, 1, 2, 2], &[1, 2], &[1, 1], &[]),
1330 "empty motif has no missing edges"
1331 );
1332 assert_eq!(
1333 cpu_ref_participation_count(3, &[0, 1, 2, 2], &[1, 2], &[1, 1], &[]),
1334 0,
1335 "empty motif has no participating nodes"
1336 );
1337 }
1338
1339 #[test]
1340 fn validate_csr_inputs_accepts_empty_and_canonical_graphs() {
1341 assert_eq!(
1342 validate_motif_inputs(0, &[0], &[], &[], &[]).unwrap(),
1343 MotifLayout {
1344 node_count: 0,
1345 output_words: 0,
1346 edge_count: 0,
1347 edge_storage_words: 1,
1348 motif_edge_count: 0,
1349 }
1350 );
1351 assert_eq!(
1352 validate_motif_inputs(
1353 3,
1354 &[0, 1, 2, 2],
1355 &[1, 2],
1356 &[1, 1],
1357 &[MotifEdge {
1358 from: 0,
1359 kind_mask: 1,
1360 to: 1,
1361 }],
1362 )
1363 .unwrap(),
1364 MotifLayout {
1365 node_count: 3,
1366 output_words: 3,
1367 edge_count: 2,
1368 edge_storage_words: 2,
1369 motif_edge_count: 1,
1370 }
1371 );
1372 }
1373
1374 #[test]
1375 fn dispatch_plan_owns_shape_grid_buffers_and_readback_words() {
1376 let motif_edges = [MotifEdge {
1377 from: 0,
1378 kind_mask: 1,
1379 to: 1,
1380 }];
1381 let launch = plan_motif_launch(
1382 3,
1383 &[0, 1, 2, 2],
1384 &[1, 2],
1385 &[1, 1],
1386 &motif_edges,
1387 "witness_out",
1388 )
1389 .expect("Fix: canonical motif launch plan should validate without materializing a Program");
1390 assert_eq!(launch.layout().node_count, 3);
1391 assert_eq!(launch.output_words(), 3);
1392 assert_eq!(launch.edge_storage_words(), 2);
1393 assert_eq!(launch.dispatch_grid(), MOTIF_DISPATCH_GRID);
1394 assert_eq!(
1395 launch.cache_key(),
1396 &MotifProgramCacheKey {
1397 node_count: 3,
1398 edge_count: 2,
1399 motif_edges: motif_edges.to_vec(),
1400 witness_out: "witness_out".to_string(),
1401 }
1402 );
1403
1404 let plan = plan_motif_dispatch(
1405 3,
1406 &[0, 1, 2, 2],
1407 &[1, 2],
1408 &[1, 1],
1409 &motif_edges,
1410 "witness_out",
1411 )
1412 .expect("Fix: canonical motif dispatch plan should validate");
1413
1414 assert_eq!(plan.layout().node_count, 3);
1415 assert_eq!(plan.layout().edge_count, 2);
1416 assert_eq!(plan.layout().motif_edge_count, 1);
1417 assert_eq!(plan.output_words(), 3);
1418 assert_eq!(plan.edge_storage_words(), 2);
1419 assert_eq!(plan.dispatch_grid(), MOTIF_DISPATCH_GRID);
1420 assert_eq!(plan.program().workgroup_size, MOTIF_WORKGROUP_SIZE);
1421 let bindings = plan
1422 .program()
1423 .buffers
1424 .iter()
1425 .map(|buffer| buffer.binding)
1426 .collect::<Vec<_>>();
1427 assert!(bindings.contains(&MOTIF_HITS_BUFFER));
1428 assert!(bindings.contains(&MOTIF_WITNESS_OUT_BUFFER));
1429
1430 let empty_edge_plan = plan_motif_dispatch(1, &[0, 0], &[], &[], &[], "witness_out")
1431 .expect("Fix: zero-edge motif graph should still have padded edge storage");
1432 assert_eq!(empty_edge_plan.layout().edge_count, 0);
1433 assert_eq!(empty_edge_plan.edge_storage_words(), 1);
1434 }
1435
1436 #[test]
1437 fn witness_participant_count_uses_primitive_contract() {
1438 assert_eq!(count_witness_participants(&[1, 0, 2, 0]).unwrap(), 2);
1439 }
1440
1441 #[test]
1442 fn validate_csr_inputs_rejects_malformed_csr() {
1443 let err = validate_csr_inputs(2, &[0, 1, 1], &[1], &[]).unwrap_err();
1444 assert!(err.contains("edge_targets.len() == edge_kind_mask.len()"));
1445
1446 let err = validate_csr_inputs(2, &[0, 2, 1], &[1], &[1]).unwrap_err();
1447 assert!(err.contains("offsets must be monotonic"));
1448
1449 let err = validate_csr_inputs(2, &[0, 1, 1], &[5], &[1]).unwrap_err();
1450 assert!(err.contains("outside node_count"));
1451 }
1452}