Skip to main content

logicaffeine_verify/
multiclock.rs

1//! Multi-Clock Domain Modeling
2//!
3//! Real designs have multiple clock domains. Each domain unrolls independently.
4//! Cross-domain references use interleaved scheduling based on clock ratios.
5//!
6//! The schedule is computed from each domain's ratio (numerator, denominator).
7//! Within one LCM period, each domain fires proportionally to its normalized rate.
8//! On global steps where a domain does NOT fire, a frame condition holds its
9//! state variables unchanged.
10
11use crate::ir::VerifyExpr;
12use crate::equivalence::Trace;
13use crate::kinduction;
14use std::collections::{HashMap, HashSet};
15
16#[derive(Debug, Clone)]
17pub struct ClockDomain {
18    pub name: String,
19    pub frequency: Option<u64>,
20    pub ratio: Option<(u32, u32)>,
21}
22
23#[derive(Debug, Clone)]
24pub struct MultiClockModel {
25    pub domains: Vec<ClockDomain>,
26    pub init: VerifyExpr,
27    pub transitions: HashMap<String, VerifyExpr>,
28    pub property: VerifyExpr,
29}
30
31#[derive(Debug)]
32pub enum MultiClockResult {
33    Safe,
34    Unsafe { trace: Trace },
35    Unknown,
36}
37
38fn gcd(a: u32, b: u32) -> u32 {
39    if b == 0 { a } else { gcd(b, a % b) }
40}
41
42fn lcm(a: u32, b: u32) -> u32 {
43    if a == 0 || b == 0 { 1 } else { a / gcd(a, b) * b }
44}
45
46/// Compute the interleaved firing schedule for a set of clock domains.
47///
48/// Returns a `Vec<Vec<bool>>` where `schedule[global_step][domain_index]`
49/// indicates whether that domain fires at that global step.
50///
51/// The schedule covers one full LCM period. Ratios are interpreted as
52/// (numerator, denominator) meaning the domain fires `numerator` times
53/// per `denominator`-unit base period. Domains without a ratio default
54/// to (1, 1).
55///
56/// Within the LCM period, the fastest domain fires every step. Slower
57/// domains fire at evenly-spaced intervals proportional to their rates.
58pub fn compute_schedule(domains: &[ClockDomain]) -> Vec<Vec<bool>> {
59    if domains.is_empty() {
60        return vec![vec![]];
61    }
62
63    let rates: Vec<(u32, u32)> = domains.iter().map(|d| {
64        d.ratio.unwrap_or((1, 1))
65    }).collect();
66
67    // Normalize rates to a common denominator, then the period is the max
68    // scaled numerator. Each domain fires scaled_rate times in period steps.
69    let common_denom: u32 = rates.iter().fold(1u32, |acc, &(_, d)| lcm(acc, d));
70    let scaled: Vec<u32> = rates.iter().map(|&(n, d)| n * (common_denom / d)).collect();
71    let period = *scaled.iter().max().unwrap_or(&1);
72
73    if period == 0 {
74        return vec![vec![false; domains.len()]];
75    }
76
77    let mut schedule = Vec::with_capacity(period as usize);
78    for step in 0..period {
79        let mut fires = Vec::with_capacity(domains.len());
80        for &fire_count in &scaled {
81            if fire_count == 0 {
82                fires.push(false);
83            } else if fire_count >= period {
84                fires.push(true);
85            } else {
86                // Bresenham-style even distribution starting at step 0.
87                // Domain fires at step s if (s * fire_count) % period < fire_count.
88                let remainder = (step as u64 * fire_count as u64) % period as u64;
89                fires.push(remainder < fire_count as u64);
90            }
91        }
92        schedule.push(fires);
93    }
94
95    schedule
96}
97
98/// Extract the set of "next-state" variable base names from a transition expression.
99///
100/// Looks for variables matching `<name>@t1` and returns the base names.
101fn extract_next_state_vars(expr: &VerifyExpr) -> HashSet<String> {
102    let mut all_vars = HashSet::new();
103    crate::equivalence::collect_vars_pub(expr, &mut all_vars);
104    let mut bases = HashSet::new();
105    for v in &all_vars {
106        if let Some(base) = v.strip_suffix("@t1") {
107            bases.insert(base.to_string());
108        }
109    }
110    bases
111}
112
113/// Build a frame condition for a domain: all its next-state variables equal current-state.
114/// `var@t1 <=> var@t` for each variable in the domain's transition.
115fn build_frame_condition(transition: &VerifyExpr) -> VerifyExpr {
116    let bases = extract_next_state_vars(transition);
117    let mut conditions: Vec<VerifyExpr> = bases.into_iter().map(|base| {
118        VerifyExpr::iff(
119            VerifyExpr::var(format!("{}@t1", base)),
120            VerifyExpr::var(format!("{}@t", base)),
121        )
122    }).collect();
123    // Sort for determinism
124    conditions.sort_by(|a, b| format!("{:?}", a).cmp(&format!("{:?}", b)));
125    if conditions.is_empty() {
126        VerifyExpr::bool(true)
127    } else {
128        conditions.into_iter().reduce(|a, b| VerifyExpr::and(a, b)).unwrap()
129    }
130}
131
132/// Verify a multi-clock domain design.
133///
134/// For single domain (or zero domains), delegates to standard k-induction.
135/// For multiple domains, computes an interleaved schedule from clock ratios
136/// and builds per-step transitions that apply each domain's transition on its
137/// fire steps and a frame condition (state held) on non-fire steps.
138pub fn verify_multiclock(model: &MultiClockModel, bound: u32) -> MultiClockResult {
139    if model.domains.len() <= 1 {
140        let transition = model.transitions.values().next()
141            .cloned()
142            .unwrap_or(VerifyExpr::bool(true));
143        let result = kinduction::k_induction(&model.init, &transition, &model.property, &[], bound);
144        match result {
145            kinduction::KInductionResult::Proven { .. } => MultiClockResult::Safe,
146            kinduction::KInductionResult::Counterexample { trace, .. } => MultiClockResult::Unsafe { trace },
147            _ => MultiClockResult::Unknown,
148        }
149    } else {
150        verify_multiclock_interleaved(model, bound)
151    }
152}
153
154/// Multi-domain verification with interleaved scheduling.
155///
156/// Algorithm:
157/// 1. Compute the schedule from domain ratios.
158/// 2. For each global step in the schedule period, build a combined transition:
159///    - If domain fires: apply its transition
160///    - If domain does not fire: apply frame condition (state held)
161/// 3. Use BMC-style unrolling with the schedule-aware transitions and
162///    check the property at each step.
163fn verify_multiclock_interleaved(model: &MultiClockModel, bound: u32) -> MultiClockResult {
164    let schedule = compute_schedule(&model.domains);
165    let period = schedule.len() as u32;
166
167    // Build per-domain frame conditions
168    let mut frame_conditions: HashMap<String, VerifyExpr> = HashMap::new();
169    for domain in &model.domains {
170        if let Some(trans) = model.transitions.get(&domain.name) {
171            frame_conditions.insert(domain.name.clone(), build_frame_condition(trans));
172        }
173    }
174
175    // Build the transition for each global step within one period.
176    // Each step's transition is the conjunction of all domains' contributions:
177    // - fire => domain's transition
178    // - !fire => frame condition
179    let mut step_transitions: Vec<VerifyExpr> = Vec::with_capacity(period as usize);
180    for step_idx in 0..period as usize {
181        let mut parts: Vec<VerifyExpr> = Vec::new();
182        for (domain_idx, domain) in model.domains.iter().enumerate() {
183            let fires = schedule.get(step_idx)
184                .and_then(|s| s.get(domain_idx))
185                .copied()
186                .unwrap_or(true);
187            if fires {
188                if let Some(trans) = model.transitions.get(&domain.name) {
189                    parts.push(trans.clone());
190                }
191            } else {
192                if let Some(frame) = frame_conditions.get(&domain.name) {
193                    parts.push(frame.clone());
194                }
195            }
196        }
197        let combined = if parts.is_empty() {
198            VerifyExpr::bool(true)
199        } else {
200            parts.into_iter().reduce(|a, b| VerifyExpr::and(a, b)).unwrap()
201        };
202        step_transitions.push(combined);
203    }
204
205    // Now run k-induction where each step uses the schedule-appropriate transition.
206    // We pass the schedule-aware transitions to a custom k-induction loop that
207    // cycles through the schedule period.
208    k_induction_with_schedule(
209        &model.init,
210        &step_transitions,
211        &model.property,
212        bound,
213    )
214}
215
216/// K-induction that cycles through a vector of transitions (one per schedule step).
217///
218/// This is like standard k-induction but the transition at global step `t` is
219/// `step_transitions[t % period]` instead of a single uniform transition.
220fn k_induction_with_schedule(
221    init: &VerifyExpr,
222    step_transitions: &[VerifyExpr],
223    property: &VerifyExpr,
224    max_k: u32,
225) -> MultiClockResult {
226    use z3::{SatResult, ast::Bool};
227
228    let period = step_transitions.len() as u32;
229    if period == 0 {
230        return MultiClockResult::Safe;
231    }
232
233
234    for k in 1..=max_k {
235        // ---- Base case ----
236        // init(0) AND T_0(0,1) AND T_1(1,2) AND ... AND T_{k-2}(k-2,k-1) AND NOT P(i) for some i
237        let base_result = {
238            let solver = crate::solver::new_solver();
239
240            let init_0 = kinduction::instantiate_at(init, 0);
241            let init_z3 = encode_to_bool_mc(&init_0);
242            solver.assert(&init_z3);
243
244            for t in 0..k.saturating_sub(1) {
245                let sched_idx = (t % period) as usize;
246                let trans = kinduction::instantiate_transition(&step_transitions[sched_idx], t);
247                let trans_z3 = encode_to_bool_mc(&trans);
248                solver.assert(&trans_z3);
249            }
250
251            let mut not_props: Vec<Bool> = Vec::new();
252            for t in 0..k {
253                let prop_t = kinduction::instantiate_at(property, t);
254                let prop_z3 = encode_to_bool_mc(&prop_t);
255                not_props.push(prop_z3.not());
256            }
257            let not_prop_refs: Vec<&Bool> = not_props.iter().collect();
258            let some_violation = Bool::or(&not_prop_refs);
259            solver.assert(&some_violation);
260
261            solver.check()
262        };
263
264        match base_result {
265            SatResult::Sat => {
266                return MultiClockResult::Unsafe {
267                    trace: Trace { cycles: vec![] },
268                };
269            }
270            SatResult::Unknown => return MultiClockResult::Unknown,
271            SatResult::Unsat => {}
272        }
273
274        // ---- Inductive step ----
275        // P(0) AND P(1) AND ... AND P(k-1) AND T_0(0,1) AND ... AND T_{k-1}(k-1,k) AND NOT P(k)
276        let step_result = {
277            let solver = crate::solver::new_solver();
278
279            for t in 0..k {
280                let prop_t = kinduction::instantiate_at(property, t);
281                let prop_z3 = encode_to_bool_mc(&prop_t);
282                solver.assert(&prop_z3);
283            }
284
285            for t in 0..k {
286                let sched_idx = (t % period) as usize;
287                let trans = kinduction::instantiate_transition(&step_transitions[sched_idx], t);
288                let trans_z3 = encode_to_bool_mc(&trans);
289                solver.assert(&trans_z3);
290            }
291
292            let prop_k = kinduction::instantiate_at(property, k);
293            let prop_k_z3 = encode_to_bool_mc(&prop_k);
294            solver.assert(&prop_k_z3.not());
295
296            solver.check()
297        };
298
299        match step_result {
300            SatResult::Unsat => {
301                return MultiClockResult::Safe;
302            }
303            SatResult::Unknown => return MultiClockResult::Unknown,
304            SatResult::Sat => {}
305        }
306    }
307
308    MultiClockResult::Unknown
309}
310
311/// Encode a VerifyExpr to Z3 Bool for multi-clock verification.
312fn encode_to_bool_mc(expr: &VerifyExpr) -> z3::ast::Bool {
313    let mut all_vars = HashSet::new();
314    crate::equivalence::collect_vars_pub(expr, &mut all_vars);
315
316    let mut bool_vars: HashMap<String, z3::ast::Bool> = HashMap::new();
317    let mut int_vars: HashMap<String, z3::ast::Int> = HashMap::new();
318
319    for name in &all_vars {
320        bool_vars.insert(name.clone(), z3::ast::Bool::new_const(name.as_str()));
321    }
322    crate::equivalence::collect_int_vars_pub(expr, &mut int_vars);
323
324    kinduction::encode_expr_bool(expr, &bool_vars, &int_vars)
325}