use std::collections::VecDeque;
use crate::spec::types::{Convention, DataType, OpSignature, OpSpec};
pub const VYRE_OP_METADATA: vyre_spec::OpMetadata = vyre_spec::OpMetadata {
id: "graph.bfs",
layer: vyre_spec::Layer::L3,
category: vyre_spec::MetadataCategory::A,
version: 1,
description: "graph bfs",
signature: "(Bytes) -> Bytes",
strictness: "strict",
archetype_signature: "(Bytes) -> Bytes",
};
pub const GOLDEN: &[vyre_spec::GoldenSample] = &[vyre_spec::GoldenSample {
op_id: "graph.bfs",
input: &[],
expected: &[0x00, 0x00, 0x00, 0x00],
reason: "empty graph frame is rejected to the zero sentinel",
}];
pub const KAT: &[vyre_spec::KatVector] = &[
vyre_spec::KatVector {
input: &[],
expected: &[0x00, 0x00, 0x00, 0x00],
source: "empty input fails MAGIC check → ValidationError::InvalidFrame → zero sentinel",
},
vyre_spec::KatVector {
input: b"VYR",
expected: &[0x00, 0x00, 0x00, 0x00],
source: "truncated magic (3 of 4 bytes) fails MAGIC length check → zero sentinel",
},
vyre_spec::KatVector {
input: &[
0x56, 0x59, 0x52, 0x47, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ],
expected: &[
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, ],
source: "2-node 1-edge graph BFS from source 0: reaches node 1 at depth 1 (hand-traced against reachable_from at line 166)",
},
vyre_spec::KatVector {
input: &[
0x56, 0x59, 0x52, 0x47, 0x04, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ],
expected: &[
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
],
source: "4-node BFS trace from unit test `computes_single_source_bfs_frame` — (0,1,1), (0,3,1), (0,2,2) in queue-pop order",
},
];
pub const ADVERSARIAL: &[vyre_spec::AdversarialInput] = &[vyre_spec::AdversarialInput {
input: b"VYR",
reason: "truncated graph magic header exercises frame rejection",
}];
pub(crate) const MAGIC: &[u8; 4] = b"VYRG";
const HEADER_WORDS: usize = 4;
pub const MAX_GRAPH_NODES: usize = 1_000_000;
#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
pub enum ValidationError {
#[error("GraphTooLarge: graph node count {actual} exceeds limit {max}. Fix: split the graph into bounded shards.")]
GraphTooLarge {
actual: usize,
max: usize,
},
#[error("invalid graph frame. Fix: encode VYRG header, bounded counts, edges, and sources.")]
InvalidFrame,
}
#[derive(Debug)]
pub struct GraphInput {
pub node_count: usize,
pub edges: Vec<(u32, u32)>,
pub sources: Vec<u32>,
pub max_depth: u32,
}
#[inline]
pub fn cpu(input: &[u8]) -> Vec<u8> {
let Ok(graph) = parse_graph_input_checked(input) else {
return vec![0; 4];
};
let Some(&source) = graph.sources.first() else {
return vec![0; 4];
};
match reachable_from(&graph, source) {
Ok(reached) => encode_reached(&reached),
Err(_) => vec![0; 4],
}
}
#[inline]
pub fn parse_graph_input(input: &[u8]) -> Option<GraphInput> {
parse_graph_input_checked(input).ok()
}
#[inline]
pub fn parse_graph_input_checked(input: &[u8]) -> Result<GraphInput, ValidationError> {
if input.len() < MAGIC.len() || &input[..MAGIC.len()] != MAGIC {
return Err(ValidationError::InvalidFrame);
}
let words = words(&input[MAGIC.len()..]).ok_or(ValidationError::InvalidFrame)?;
if words.len() < HEADER_WORDS {
return Err(ValidationError::InvalidFrame);
}
let node_count = words[0] as usize;
if node_count > MAX_GRAPH_NODES {
return Err(ValidationError::GraphTooLarge {
actual: node_count,
max: MAX_GRAPH_NODES,
});
}
let edge_count = words[1] as usize;
let source_count = words[2] as usize;
let max_depth = words[3];
let required = HEADER_WORDS
.checked_add(
edge_count
.checked_mul(2)
.ok_or(ValidationError::InvalidFrame)?,
)
.ok_or(ValidationError::InvalidFrame)?
.checked_add(source_count)
.ok_or(ValidationError::InvalidFrame)?;
if words.len() < required {
return Err(ValidationError::InvalidFrame);
}
let mut edges = Vec::with_capacity(edge_count);
let mut offset = HEADER_WORDS;
for _ in 0..edge_count {
let from = words[offset];
let to = words[offset + 1];
if from as usize >= node_count || to as usize >= node_count {
return Err(ValidationError::InvalidFrame);
}
edges.push((from, to));
offset += 2;
}
let mut sources = Vec::with_capacity(source_count);
for &source in &words[offset..offset + source_count] {
if source as usize >= node_count {
return Err(ValidationError::InvalidFrame);
}
sources.push(source);
}
Ok(GraphInput {
node_count,
edges,
sources,
max_depth,
})
}
#[inline]
pub fn reachable_from(
graph: &GraphInput,
source: u32,
) -> Result<Vec<(u32, u32, u32)>, ValidationError> {
if graph.node_count > MAX_GRAPH_NODES {
return Err(ValidationError::GraphTooLarge {
actual: graph.node_count,
max: MAX_GRAPH_NODES,
});
}
if source as usize >= graph.node_count {
return Err(ValidationError::InvalidFrame);
}
let mut adjacency = vec![Vec::<u32>::new(); graph.node_count];
for &(from, to) in &graph.edges {
adjacency[from as usize].push(to);
}
let mut reached = Vec::new();
let mut visited = vec![false; graph.node_count];
let mut queue = VecDeque::new();
visited[source as usize] = true;
queue.push_back((source, 0u32));
while let Some((node, depth)) = queue.pop_front() {
if node != source {
reached.push((source, node, depth));
}
if depth >= graph.max_depth {
continue;
}
for &target in &adjacency[node as usize] {
let target_idx = target as usize;
if !visited[target_idx] {
visited[target_idx] = true;
queue.push_back((target, depth.saturating_add(1)));
}
}
}
Ok(reached)
}
#[inline]
pub fn encode_reached(reached: &[(u32, u32, u32)]) -> Vec<u8> {
let mut bytes = Vec::with_capacity(reached.len() * 12);
for &(source, node, depth) in reached {
bytes.extend_from_slice(&source.to_le_bytes());
bytes.extend_from_slice(&node.to_le_bytes());
bytes.extend_from_slice(&depth.to_le_bytes());
}
bytes
}
fn words(input: &[u8]) -> Option<Vec<u32>> {
if input.len() % 4 != 0 {
return None;
}
Some(
input
.chunks_exact(4)
.map(|word| u32::from_le_bytes([word[0], word[1], word[2], word[3]]))
.collect(),
)
}
fn wgsl() -> String {
"fn vyre_op(index: u32, input_len: u32) -> u32 {
if index >= input_len { return 0u; }
return input.data[index];
}"
.to_string()
}
#[inline]
pub fn vyre_op() -> OpSpec {
let id = "graph.bfs";
OpSpec::builder(id)
.signature(OpSignature {
inputs: vec![DataType::Bytes],
output: DataType::Bytes,
})
.cpu_fn(cpu)
.wgsl_fn(wgsl)
.category(crate::Category::A {
composition_of: vec![id],
})
.laws(vec![crate::spec::law::AlgebraicLaw::Bounded {
lo: 0,
hi: u32::MAX,
}])
.strictness(crate::spec::types::Strictness::Strict)
.version(1)
.alt_wgsl_fns(vec![("category_a_handwritten", wgsl)])
.convention(Convention::default())
.workgroup_size(Some(1))
.boundary_values(vec![
crate::spec::types::BoundaryValue {
label: "empty",
inputs: vec![0],
},
crate::spec::types::BoundaryValue {
label: "single_element",
inputs: vec![1],
},
crate::spec::types::BoundaryValue {
label: "boundary",
inputs: vec![255],
},
crate::spec::types::BoundaryValue {
label: "max",
inputs: vec![u32::MAX],
},
])
.equivalence_classes(vec![
crate::spec::types::EquivalenceClass::specific("empty input", vec![0]),
crate::spec::types::EquivalenceClass::specific("typical input", vec![42]),
crate::spec::types::EquivalenceClass::specific("boundary input", vec![255]),
])
.expect("Fix: checked-in conform spec must satisfy the typestate builder")
}
#[cfg(test)]
mod tests {
use super::{cpu, encode_reached, MAGIC};
#[test]
fn computes_single_source_bfs_frame() {
let mut input = MAGIC.to_vec();
for word in [4u32, 3, 1, 8, 0, 1, 1, 2, 0, 3, 0] {
input.extend_from_slice(&word.to_le_bytes());
}
assert_eq!(
cpu(&input),
encode_reached(&[(0, 1, 1), (0, 3, 1), (0, 2, 2)])
);
}
}
#[cfg(test)]
mod proptests {
#[test]
fn coverage_artifacts_are_registered() {
assert!(!super::KAT.is_empty());
assert!(!super::ADVERSARIAL.is_empty());
}
}