1use std::sync::Arc;
16
17use vyre_foundation::ir::Program;
18
19use crate::backend::{BackendError, CompiledPipeline, DispatchConfig};
20
21type ReferenceRunFn =
22 dyn Fn(&Program, &[Vec<u8>]) -> Result<Vec<Vec<u8>>, BackendError> + Send + Sync;
23
24#[derive(Clone)]
30pub struct ReferenceExecutor {
31 run: Arc<ReferenceRunFn>,
32}
33
34impl ReferenceExecutor {
35 pub fn new<F>(run: F) -> Self
37 where
38 F: Fn(&Program, &[Vec<u8>]) -> Result<Vec<Vec<u8>>, BackendError> + Send + Sync + 'static,
39 {
40 Self { run: Arc::new(run) }
41 }
42
43 pub fn run(&self, program: &Program, inputs: &[Vec<u8>]) -> Result<Vec<Vec<u8>>, BackendError> {
51 (self.run)(program, inputs)
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct ConformanceCase {
58 label: String,
59 inputs: Vec<Vec<u8>>,
60}
61
62impl ConformanceCase {
63 #[must_use]
65 pub fn new(label: impl Into<String>, inputs: Vec<Vec<u8>>) -> Self {
66 Self {
67 label: label.into(),
68 inputs,
69 }
70 }
71
72 #[must_use]
74 pub fn label(&self) -> &str {
75 &self.label
76 }
77
78 #[must_use]
80 pub fn inputs(&self) -> &[Vec<u8>] {
81 &self.inputs
82 }
83}
84
85#[derive(Debug, Clone, Default, PartialEq, Eq)]
87pub struct ConformanceMatrix {
88 cases: Vec<ConformanceCase>,
89}
90
91impl ConformanceMatrix {
92 #[must_use]
94 pub fn new(cases: Vec<ConformanceCase>) -> Self {
95 Self { cases }
96 }
97
98 pub fn push(&mut self, case: ConformanceCase) {
100 self.cases.push(case);
101 }
102
103 #[must_use]
105 pub fn cases(&self) -> &[ConformanceCase] {
106 &self.cases
107 }
108
109 #[must_use]
111 pub fn is_empty(&self) -> bool {
112 self.cases.is_empty()
113 }
114}
115
116#[derive(Debug, Clone, PartialEq, Eq)]
118pub struct DivergenceEvent {
119 pub case_label: String,
121 pub program_fingerprint: [u8; 32],
123 pub inputs: Vec<Vec<u8>>,
125 pub backend_output: Vec<Vec<u8>>,
127 pub reference_output: Vec<Vec<u8>>,
129}
130
131#[derive(Debug, thiserror::Error)]
133pub enum ConformanceError {
134 #[error(
136 "conformance matrix is empty. Fix: populate every op with at least one witness tuple from vyre-conform-spec before asserting backend parity."
137 )]
138 EmptyMatrix,
139 #[error(
141 "backend rejected witness `{case_label}`: {source}. Fix: the backend must accept every witness tuple the reference accepts for this Program."
142 )]
143 BackendRejected {
144 case_label: String,
146 #[source]
148 source: BackendError,
149 },
150 #[error(
152 "reference rejected witness `{case_label}`: {source}. Fix: inspect the Program body or witness tuple; the reference is the contract oracle for exhaustive conformance."
153 )]
154 ReferenceRejected {
155 case_label: String,
157 #[source]
159 source: BackendError,
160 },
161 #[error(
163 "backend diverged from the reference on witness `{event_case_label}`. Fix: inspect the embedded outputs and repair the backend until every witness tuple is byte-identical."
164 )]
165 Diverged {
166 event: Box<DivergenceEvent>,
168 event_case_label: String,
170 },
171}
172
173pub fn assert_exhaustive_byte_identity(
184 pipeline: &dyn CompiledPipeline,
185 program: &Program,
186 reference: &ReferenceExecutor,
187 matrix: &ConformanceMatrix,
188 config: &DispatchConfig,
189) -> Result<(), ConformanceError> {
190 if matrix.is_empty() {
191 return Err(ConformanceError::EmptyMatrix);
192 }
193
194 let program_fingerprint = program_fingerprint(program);
195 let mut first_error = None;
196 for case in matrix.cases() {
197 let backend_output = match pipeline.dispatch(case.inputs(), config) {
198 Ok(output) => output,
199 Err(source) => {
200 first_error.get_or_insert(ConformanceError::BackendRejected {
201 case_label: case.label().to_string(),
202 source,
203 });
204 continue;
205 }
206 };
207 let reference_output = match reference.run(program, case.inputs()) {
208 Ok(output) => output,
209 Err(source) => {
210 first_error.get_or_insert(ConformanceError::ReferenceRejected {
211 case_label: case.label().to_string(),
212 source,
213 });
214 continue;
215 }
216 };
217 if backend_output != reference_output {
218 let event = DivergenceEvent {
219 case_label: case.label().to_string(),
220 program_fingerprint,
221 inputs: case.inputs().to_vec(),
222 backend_output,
223 reference_output,
224 };
225 first_error.get_or_insert(ConformanceError::Diverged {
226 event_case_label: event.case_label.clone(),
227 event: Box::new(event),
228 });
229 }
230 }
231
232 first_error.map_or(Ok(()), Err)
233}
234
235fn program_fingerprint(program: &Program) -> [u8; 32] {
236 vyre_foundation::optimizer::pipeline_fingerprint_bytes(program)
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use std::sync::Mutex;
243
244 use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
245
246 type FakeRun = dyn Fn(&[Vec<u8>]) -> Result<Vec<Vec<u8>>, BackendError> + Send + Sync;
247
248 struct FakePipeline {
249 id: String,
250 run: Arc<FakeRun>,
251 }
252
253 impl crate::backend::private::Sealed for FakePipeline {}
254
255 impl CompiledPipeline for FakePipeline {
256 fn id(&self) -> &str {
257 &self.id
258 }
259
260 fn dispatch(
261 &self,
262 inputs: &[Vec<u8>],
263 _config: &DispatchConfig,
264 ) -> Result<Vec<Vec<u8>>, BackendError> {
265 (self.run)(inputs)
266 }
267 }
268
269 fn sample_program() -> Program {
270 Program::wrapped(
271 vec![
272 BufferDecl::storage("input", 0, BufferAccess::ReadOnly, DataType::U32)
273 .with_count(1),
274 BufferDecl::storage("output", 1, BufferAccess::ReadWrite, DataType::U32)
275 .with_count(1),
276 ],
277 [1, 1, 1],
278 vec![Node::store(
279 "output",
280 Expr::u32(0),
281 Expr::load("input", Expr::u32(0)),
282 )],
283 )
284 }
285
286 fn witness_matrix() -> ConformanceMatrix {
287 ConformanceMatrix::new(
288 u32_witnesses()
289 .into_iter()
290 .map(|witness| {
291 ConformanceCase::new(
292 format!("u32:{witness:#010x}"),
293 vec![witness.to_le_bytes().to_vec()],
294 )
295 })
296 .collect(),
297 )
298 }
299
300 #[test]
301 fn empty_matrix_is_rejected() {
302 let pipeline: Arc<dyn CompiledPipeline> = Arc::new(FakePipeline {
303 id: "fake".into(),
304 run: Arc::new(|inputs| Ok(inputs.to_vec())),
305 });
306 let reference = ReferenceExecutor::new(|_, inputs| Ok(inputs.to_vec()));
307
308 let error = assert_exhaustive_byte_identity(
309 pipeline.as_ref(),
310 &sample_program(),
311 &reference,
312 &ConformanceMatrix::default(),
313 &DispatchConfig::default(),
314 )
315 .expect_err("empty witness inventories must be rejected");
316
317 assert!(matches!(error, ConformanceError::EmptyMatrix));
318 }
319
320 #[test]
321 fn exhaustive_matrix_passes_matching_outputs() {
322 let pipeline: Arc<dyn CompiledPipeline> = Arc::new(FakePipeline {
323 id: "fake".into(),
324 run: Arc::new(|inputs| Ok(inputs.to_vec())),
325 });
326 let reference = ReferenceExecutor::new(|_, inputs| Ok(inputs.to_vec()));
327
328 assert_exhaustive_byte_identity(
329 pipeline.as_ref(),
330 &sample_program(),
331 &reference,
332 &witness_matrix(),
333 &DispatchConfig::default(),
334 )
335 .expect("Fix: matching backend/reference outputs must pass the exhaustive matrix; restore this invariant before continuing.");
336 }
337
338 #[test]
339 fn exhaustive_matrix_catches_divergence_hidden_by_sampling() {
340 let hidden_witness = 0xDEAD_BEEF_u32.to_le_bytes().to_vec();
341 let seen = Arc::new(Mutex::new(Vec::<Vec<u8>>::new()));
342 let seen_clone = Arc::clone(&seen);
343 let pipeline: Arc<dyn CompiledPipeline> = Arc::new(FakePipeline {
344 id: "fake".into(),
345 run: Arc::new(move |inputs| {
346 seen_clone.lock().unwrap().push(inputs[0].clone());
347 if inputs[0] == hidden_witness {
348 Ok(vec![0_u32.to_le_bytes().to_vec()])
349 } else {
350 Ok(inputs.to_vec())
351 }
352 }),
353 });
354 let reference = ReferenceExecutor::new(|_, inputs| Ok(inputs.to_vec()));
355
356 let error = assert_exhaustive_byte_identity(
357 pipeline.as_ref(),
358 &sample_program(),
359 &reference,
360 &witness_matrix(),
361 &DispatchConfig::default(),
362 )
363 .expect_err("one divergent witness must fail exhaustive conformance");
364
365 match error {
366 ConformanceError::Diverged { event, .. } => {
367 assert_eq!(event.case_label, "u32:0xdeadbeef");
368 assert_eq!(event.inputs, vec![0xDEAD_BEEF_u32.to_le_bytes().to_vec()]);
369 assert_eq!(event.backend_output, vec![0_u32.to_le_bytes().to_vec()]);
370 assert_eq!(
371 event.reference_output,
372 vec![0xDEAD_BEEF_u32.to_le_bytes().to_vec()]
373 );
374 }
375 other => panic!("expected divergence event, got {other:?}"),
376 }
377
378 assert_eq!(
379 seen.lock().unwrap().len(),
380 u32_witnesses().len(),
381 "the conformance matrix must execute every witness tuple exactly once"
382 );
383 }
384
385 fn u32_witnesses() -> Vec<u32> {
386 let mut out = vec![
387 0u32,
388 1,
389 2,
390 3,
391 u32::MAX,
392 u32::MAX - 1,
393 0x8000_0000,
394 0x7FFF_FFFF,
395 0xAAAA_AAAA,
396 0x5555_5555,
397 0xDEAD_BEEF,
398 0xCAFE_F00D,
399 ];
400 let mut state = 0xD5E4_A7B9_3C6D_102Fu64;
401 for _ in 0..24 {
402 state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
403 let mut z = state;
404 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
405 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
406 z ^= z >> 31;
407 out.push((z as u32) ^ ((z >> 32) as u32));
408 }
409 out
410 }
411}