Skip to main content

bitloom_sim/
formal_equiv.rs

1//! FR100 formal-equivalence product path (NFR14 F1–F5).
2//!
3//! **F1 branch (i):** in-tree bounded prover API — beyond PortValues random
4//! scoreboard alone. Exhaustive enumeration over a doc-pinned boolean input
5//! alphabet × depth proves FL ≡ cycle-accurate `tick` within the bound.
6//!
7//! The automatic **random/compare** path is a companion (F3) and **≠** FR100
8//! close by itself (F5). FR92 `SharedStimulusScoreboard` remains supporting /
9//! not sufficient for FR100.
10//!
11//! Observation surface: `PortValues` only (AD-17). Design crates stay on
12//! `bitloom-prelude`; this module lives in the toolchain (`bitloom-sim`).
13
14use bitloom_hir::{FrozenHir, PortValues};
15
16use crate::{
17    AbstractionView, EquivStatus, check_functional_equiv, check_functional_equiv_generated,
18    check_generated_bridge_with,
19};
20
21/// Product entry for FR100 automatic compare + bounded formal equivalence.
22///
23/// Not a rename of [`crate::SharedStimulusScoreboard`]: random sampling and
24/// bounded exhaustive exploration are distinct surfaces; only the latter is the
25/// F1-(i) formal product entry.
26#[derive(Debug, Clone)]
27pub struct FormalEquivProduct {
28    /// Deterministic seed for [`Self::check_random_compare`].
29    pub seed: u64,
30    /// Number of stimulus frames for the random/compare companion path.
31    pub random_cycles: usize,
32    /// Boolean input ports forming the exhaustive alphabet (typically `rst`).
33    boolean_ports: Vec<String>,
34    /// Sequence length for bounded exhaustive exploration.
35    exhaustive_depth: usize,
36}
37
38impl FormalEquivProduct {
39    /// Build with seed + random cycle count; default alphabet empty until
40    /// [`Self::with_boolean_ports`].
41    pub fn new(seed: u64, random_cycles: usize) -> Self {
42        Self {
43            seed,
44            random_cycles,
45            boolean_ports: Vec::new(),
46            exhaustive_depth: 0,
47        }
48    }
49
50    /// Pin boolean ports that form the exhaustive / random alphabet.
51    pub fn with_boolean_ports(mut self, ports: &[&str]) -> Self {
52        self.boolean_ports = ports.iter().map(|p| (*p).to_string()).collect();
53        self
54    }
55
56    /// Set bounded-exhaustive sequence depth (F1-(i) / F4 fixture scale).
57    pub fn with_exhaustive_depth(mut self, depth: usize) -> Self {
58        self.exhaustive_depth = depth;
59        self
60    }
61
62    /// Alphabet of single-frame `PortValues` (cartesian product of 0/1 on each
63    /// boolean port). Empty ports → one default empty frame.
64    pub fn alphabet(&self) -> Vec<PortValues> {
65        if self.boolean_ports.is_empty() {
66            return vec![PortValues::default()];
67        }
68        let n = self.boolean_ports.len();
69        assert!(
70            n <= 12,
71            "FormalEquivProduct alphabet supports at most 12 boolean ports (got {n}); F4 MVP scale"
72        );
73        let combinations = 1usize << n;
74        let mut out = Vec::with_capacity(combinations);
75        for mask in 0..combinations {
76            let mut pv = PortValues::default();
77            for (i, name) in self.boolean_ports.iter().enumerate() {
78                let bit = ((mask >> i) & 1) as u64;
79                pv.set(name, bit);
80            }
81            out.push(pv);
82        }
83        out
84    }
85
86    /// Deterministic random stimuli from the alphabet (LCG; companion F3 path).
87    pub fn random_stimuli(&self) -> Vec<PortValues> {
88        let alphabet = self.alphabet();
89        let mut state = self.seed;
90        let mut out = Vec::with_capacity(self.random_cycles.max(1));
91        let n = self.random_cycles.max(1);
92        for _ in 0..n {
93            // Numerical Recipes LCG
94            state = state.wrapping_mul(1664525).wrapping_add(1013904223);
95            let idx = (state as usize) % alphabet.len();
96            out.push(alphabet[idx].clone());
97        }
98        out
99    }
100
101    /// F3 companion: automatic random PortValues compare (generated FL vs tick).
102    ///
103    /// Reproducible for a fixed seed. **Not** sufficient alone to close FR100 (F5).
104    pub fn check_random_compare(&self, hir: FrozenHir) -> EquivStatus {
105        check_functional_equiv_generated(hir, self.random_stimuli())
106    }
107
108    /// Same as [`Self::check_random_compare`] with an arbitrary functional view
109    /// (deliberate-mismatch ATDD).
110    pub fn check_random_compare_with<A: AbstractionView>(
111        &self,
112        hir: FrozenHir,
113        abs: &mut A,
114    ) -> EquivStatus {
115        check_generated_bridge_with(hir, abs, self.random_stimuli())
116    }
117
118    /// F1-(i) product entry: exhaustive FL≡tick over all alphabet^depth sequences.
119    ///
120    /// Beyond random scoreboard sampling: every sequence of length
121    /// `exhaustive_depth` drawn from the pinned alphabet is checked. First
122    /// failing sequence returns `Fail` with readable `PortMismatch`s.
123    pub fn check_bounded_exhaustive(&self, hir: FrozenHir) -> EquivStatus {
124        let sequences = self.exhaustive_sequences();
125        let mut total_cycles = 0usize;
126        for seq in sequences {
127            match check_functional_equiv_generated(hir.clone(), seq) {
128                EquivStatus::Pass { cycles } => total_cycles = total_cycles.saturating_add(cycles),
129                fail @ EquivStatus::Fail { .. } => return fail,
130            }
131        }
132        EquivStatus::Pass {
133            cycles: total_cycles,
134        }
135    }
136
137    /// Bounded exhaustive with an arbitrary functional view (mismatch ATDD).
138    pub fn check_bounded_exhaustive_with<A: AbstractionView>(
139        &self,
140        hir: FrozenHir,
141        abs: &mut A,
142    ) -> EquivStatus {
143        let sequences = self.exhaustive_sequences();
144        let mut total_cycles = 0usize;
145        for seq in sequences {
146            match check_functional_equiv(hir.clone(), abs, seq) {
147                EquivStatus::Pass { cycles } => total_cycles = total_cycles.saturating_add(cycles),
148                fail @ EquivStatus::Fail { .. } => return fail,
149            }
150        }
151        EquivStatus::Pass {
152            cycles: total_cycles,
153        }
154    }
155
156    /// All sequences of length `exhaustive_depth` over [`Self::alphabet`].
157    pub fn exhaustive_sequences(&self) -> Vec<Vec<PortValues>> {
158        let alphabet = self.alphabet();
159        let depth = self.exhaustive_depth;
160        if depth == 0 {
161            return vec![Vec::new()];
162        }
163        let mut sequences: Vec<Vec<PortValues>> = vec![Vec::new()];
164        for _ in 0..depth {
165            let mut next = Vec::with_capacity(sequences.len() * alphabet.len());
166            for prefix in &sequences {
167                for frame in &alphabet {
168                    let mut seq = prefix.clone();
169                    seq.push(frame.clone());
170                    next.push(seq);
171                }
172            }
173            sequences = next;
174        }
175        sequences
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn alphabet_two_ports_has_four_frames() {
185        let p = FormalEquivProduct::new(0, 0).with_boolean_ports(&["rst", "en"]);
186        assert_eq!(p.alphabet().len(), 4);
187    }
188
189    #[test]
190    fn exhaustive_depth_two_bool_has_four_sequences() {
191        let p = FormalEquivProduct::new(0, 0)
192            .with_boolean_ports(&["rst"])
193            .with_exhaustive_depth(2);
194        assert_eq!(p.exhaustive_sequences().len(), 4);
195    }
196
197    #[test]
198    fn random_stimuli_reproducible() {
199        let p = FormalEquivProduct::new(42, 5).with_boolean_ports(&["rst"]);
200        assert_eq!(p.random_stimuli(), p.random_stimuli());
201    }
202}