Skip to main content

vyre_driver/
shadow.rs

1//! Exhaustive CPU-vs-backend conformance for compiled pipelines.
2//!
3//! The old sampled shadow path compared a runtime sample of live
4//! dispatches. That could never prove soundness: a backend bug whose
5//! divergence rate stayed below the sample rate would slip through.
6//!
7//! This module replaces sampling with an explicit conformance matrix.
8//! Callers build a deterministic set of witness cases, run the backend
9//! and reference on every case, and require byte-identical outputs for
10//! every tuple. The canonical witness inventory lives in
11//! `vyre-conform-spec`; this module stays substrate-neutral by
12//! accepting the concrete matrix as input rather than depending on a
13//! particular op inventory at runtime.
14
15use 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/// Executor that runs `program` on a CPU-side reference interpreter.
25///
26/// `vyre-reference::reference_eval` is the canonical implementation. A host
27/// wires an adapter into this wrapper so the conformance path stays
28/// substrate-neutral (no vyre-driver → vyre-reference dep cycle).
29#[derive(Clone)]
30pub struct ReferenceExecutor {
31    run: Arc<ReferenceRunFn>,
32}
33
34impl ReferenceExecutor {
35    /// Build a concrete reference-execution adapter.
36    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    /// Execute `program` against `inputs`, returning the byte-level
44    /// output buffers in the same order the backend would emit.
45    ///
46    /// # Errors
47    ///
48    /// Returns a [`BackendError`] when the reference rejects the
49    /// program or any witness tuple.
50    pub fn run(&self, program: &Program, inputs: &[Vec<u8>]) -> Result<Vec<Vec<u8>>, BackendError> {
51        (self.run)(program, inputs)
52    }
53}
54
55/// One deterministic witness case in an exhaustive conformance run.
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct ConformanceCase {
58    label: String,
59    inputs: Vec<Vec<u8>>,
60}
61
62impl ConformanceCase {
63    /// Build one named witness tuple.
64    #[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    /// Stable label used in diagnostics.
73    #[must_use]
74    pub fn label(&self) -> &str {
75        &self.label
76    }
77
78    /// Input buffers in declaration order.
79    #[must_use]
80    pub fn inputs(&self) -> &[Vec<u8>] {
81        &self.inputs
82    }
83}
84
85/// Deterministic witness inventory for a compiled pipeline.
86#[derive(Debug, Clone, Default, PartialEq, Eq)]
87pub struct ConformanceMatrix {
88    cases: Vec<ConformanceCase>,
89}
90
91impl ConformanceMatrix {
92    /// Build a matrix from an explicit witness list.
93    #[must_use]
94    pub fn new(cases: Vec<ConformanceCase>) -> Self {
95        Self { cases }
96    }
97
98    /// Append one witness case.
99    pub fn push(&mut self, case: ConformanceCase) {
100        self.cases.push(case);
101    }
102
103    /// Borrow the deterministic witness list.
104    #[must_use]
105    pub fn cases(&self) -> &[ConformanceCase] {
106        &self.cases
107    }
108
109    /// Whether the matrix is empty.
110    #[must_use]
111    pub fn is_empty(&self) -> bool {
112        self.cases.is_empty()
113    }
114}
115
116/// Structured divergence surfaced by the exhaustive matrix.
117#[derive(Debug, Clone, PartialEq, Eq)]
118pub struct DivergenceEvent {
119    /// Stable label of the witness tuple that diverged.
120    pub case_label: String,
121    /// blake3 fingerprint of the Program's canonical wire bytes.
122    pub program_fingerprint: [u8; 32],
123    /// Input buffers supplied to the dispatch, in declaration order.
124    pub inputs: Vec<Vec<u8>>,
125    /// Outputs the backend produced.
126    pub backend_output: Vec<Vec<u8>>,
127    /// Outputs the reference produced.
128    pub reference_output: Vec<Vec<u8>>,
129}
130
131/// Exhaustive conformance failures.
132#[derive(Debug, thiserror::Error)]
133pub enum ConformanceError {
134    /// The caller supplied no witness tuples.
135    #[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    /// The backend rejected a witness tuple.
140    #[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        /// Stable label of the failing witness tuple.
145        case_label: String,
146        /// Backend error.
147        #[source]
148        source: BackendError,
149    },
150    /// The reference rejected a witness tuple.
151    #[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        /// Stable label of the failing witness tuple.
156        case_label: String,
157        /// Reference error.
158        #[source]
159        source: BackendError,
160    },
161    /// Backend and reference both ran but produced different bytes.
162    #[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        /// Detailed byte-level divergence.
167        event: Box<DivergenceEvent>,
168        /// Shadow field used by the display impl without reformatting the full event.
169        event_case_label: String,
170    },
171}
172
173/// Run the backend and reference across every witness tuple in `matrix`.
174///
175/// This is intentionally exhaustive over the supplied cases: if a caller wants
176/// "sampled" behaviour, they must sample before constructing the matrix. The
177/// conformance harness itself never drops a case.
178///
179/// # Errors
180///
181/// Returns the first [`ConformanceError`] after every witness has been
182/// executed.
183pub 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}