Skip to main content

logicaffeine_compile/codegen_sva/
controller_gen.rs

1//! Designer → certified controller: turn a synthesized [`PhasePlan`] into a Verilog phase-FSM and
2//! let the existing RTL pipeline (`parse_transition_system` → `prove_invariant`) certify it.
3//!
4//! The controller cycles a `phase` counter; on entering each phase it greens *exactly* that
5//! phase's movements and reds the rest. Because the plan is a conflict-free colouring, no
6//! conflicting pair is ever green together — and because every transition fully rewrites all
7//! movement registers to a (safe) phase configuration, the safety property is **1-inductive**, so
8//! `prove_invariant(1)` proves it with no extra invariant strengthening. This closes the loop:
9//! design (SAT) → generate (codegen) → prove (k-induction), entirely on our own stack.
10
11use super::signal_design::{Intersection, PhasePlan};
12use std::fmt::Write as _;
13
14/// Generate a synthesizable Verilog controller (one 1-bit `mvN` register per movement, a `phase`
15/// counter) whose conflict-freedom is provable by `prove_invariant`.
16pub fn generate_controller(it: &Intersection, plan: &PhasePlan) -> String {
17    let n = it.movements.len();
18    let k = plan.num_phases.max(1);
19    let groups = plan.groups();
20    let bits = phase_bits(k);
21
22    let mut v = String::new();
23    let _ = writeln!(v, "module controller(input clk);");
24    let _ = writeln!(v, "  reg [{}:0] phase;", bits - 1);
25    for i in 0..n {
26        let _ = writeln!(v, "  reg mv{i};");
27    }
28    let _ = writeln!(
29        v,
30        "  initial begin phase = {bits}'d0; {} end",
31        config_assigns(&groups, 0, n, true)
32    );
33    let _ = writeln!(v, "  always @(posedge clk)");
34    if k == 1 {
35        // No conflicts: a single phase greens everything, every cycle.
36        let _ = writeln!(
37            v,
38            "    begin phase <= {bits}'d0; {} end",
39            config_assigns(&groups, 0, n, false)
40        );
41    } else {
42        for p in 0..(k - 1) {
43            let kw = if p == 0 { "if" } else { "else if" };
44            let _ = writeln!(
45                v,
46                "    {kw} (phase == {bits}'d{p}) begin phase <= {bits}'d{}; {} end",
47                p + 1,
48                config_assigns(&groups, p + 1, n, false)
49            );
50        }
51        // Last phase (and any unreachable out-of-range counter value) wraps to phase 0.
52        let _ = writeln!(
53            v,
54            "    else begin phase <= {bits}'d0; {} end",
55            config_assigns(&groups, 0, n, false)
56        );
57    }
58    let _ = writeln!(v, "  assert property ({});", conflict_free_property(it));
59    let _ = writeln!(v, "endmodule");
60    v
61}
62
63/// Assign every movement register for phase `p`: `1` if served in that phase, else `0`.
64/// `blocking` selects `=` (an `initial` block) vs `<=` (an `always` block).
65fn config_assigns(groups: &[Vec<usize>], p: usize, n: usize, blocking: bool) -> String {
66    let op = if blocking { "=" } else { "<=" };
67    let served: std::collections::HashSet<usize> =
68        groups.get(p).map(|g| g.iter().copied().collect()).unwrap_or_default();
69    (0..n)
70        .map(|i| format!("mv{i} {op} 1'd{};", usize::from(served.contains(&i))))
71        .collect::<Vec<_>>()
72        .join(" ")
73}
74
75/// The safety property: no conflicting pair is green together. A conflict-free intersection has
76/// nothing to violate, so a tautology stands in.
77fn conflict_free_property(it: &Intersection) -> String {
78    if it.conflicts.is_empty() {
79        "mv0 | ~mv0".to_string()
80    } else {
81        it.conflicts
82            .iter()
83            .map(|&(a, b)| format!("~(mv{a} & mv{b})"))
84            .collect::<Vec<_>>()
85            .join(" & ")
86    }
87}
88
89/// Bits needed for a phase counter over `k` phases (values `0..k`).
90fn phase_bits(k: usize) -> usize {
91    let mut bits = 1;
92    while (1usize << bits) < k {
93        bits += 1;
94    }
95    bits
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use crate::codegen_sva::rtl::parse_transition_system;
102    use crate::codegen_sva::signal_design::design_phase_plan;
103    use logicaffeine_proof::bmc::InductionOutcome;
104
105    fn graph(n: usize, conflicts: &[(usize, usize)]) -> Intersection {
106        Intersection {
107            movements: (0..n).map(|i| format!("m{i}")).collect(),
108            conflicts: conflicts.iter().map(|&(a, b)| (a.min(b), a.max(b))).collect(),
109        }
110    }
111
112    /// The crown jewel: design (SAT) → generate (codegen) → prove (k-induction), all green.
113    #[test]
114    fn generated_controller_is_proven_conflict_free() {
115        let graphs = [
116            graph(3, &[]),                                        // conflict-free → 1 phase
117            graph(2, &[(0, 1)]),                                  // 2 phases
118            graph(3, &[(0, 1), (1, 2), (0, 2)]),                  // triangle → 3
119            graph(4, &[(0, 1), (1, 2), (2, 3), (3, 0)]),          // even cycle → 2
120            graph(5, &[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]),  // odd cycle → 3
121            graph(4, &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]), // K4 → 4
122            graph(5, &[(0, 1), (0, 2), (0, 3), (0, 4)]),          // star → 2
123        ];
124        for g in graphs {
125            let plan = design_phase_plan(&g).unwrap();
126            let verilog = generate_controller(&g, &plan);
127            let ts = parse_transition_system(&verilog)
128                .unwrap_or_else(|e| panic!("generated controller did not parse: {}\n{verilog}", e.message));
129            assert_eq!(
130                ts.prove_invariant(1),
131                InductionOutcome::Proven,
132                "generated controller must be PROVEN conflict-free:\n{verilog}"
133            );
134        }
135    }
136
137    #[test]
138    fn generated_verilog_is_well_formed() {
139        let g = graph(3, &[(0, 1), (1, 2), (0, 2)]);
140        let plan = design_phase_plan(&g).unwrap();
141        let v = generate_controller(&g, &plan);
142        assert!(v.contains("module controller(input clk);"), "{v}");
143        assert!(v.contains("endmodule"));
144        assert!(v.contains("assert property"));
145        for i in 0..3 {
146            assert!(v.contains(&format!("reg mv{i};")), "missing mv{i}:\n{v}");
147        }
148        // The triangle's safety property names all three conflicts.
149        assert!(v.contains("~(mv0 & mv1)") && v.contains("~(mv1 & mv2)") && v.contains("~(mv0 & mv2)"), "{v}");
150    }
151
152    #[test]
153    fn a_deliberately_unsafe_controller_is_caught() {
154        // Sanity that the property has teeth: hand-mangle the generated RTL so two conflicting
155        // movements are both green, and confirm the prover rejects it.
156        let g = graph(2, &[(0, 1)]);
157        let plan = design_phase_plan(&g).unwrap();
158        let good = generate_controller(&g, &plan);
159        // Force every phase to green BOTH movements.
160        let bad = good
161            .replace("mv0 <= 1'd0;", "mv0 <= 1'd1;")
162            .replace("mv1 <= 1'd0;", "mv1 <= 1'd1;")
163            .replace("mv0 = 1'd0;", "mv0 = 1'd1;")
164            .replace("mv1 = 1'd0;", "mv1 = 1'd1;");
165        assert_ne!(bad, good, "the mangle must change something");
166        let ts = parse_transition_system(&bad).expect("still parses");
167        assert_ne!(
168            ts.prove_invariant(1),
169            InductionOutcome::Proven,
170            "a both-green controller must NOT prove safe:\n{bad}"
171        );
172    }
173
174    #[test]
175    fn phase_bits_are_correct() {
176        assert_eq!(phase_bits(1), 1);
177        assert_eq!(phase_bits(2), 1);
178        assert_eq!(phase_bits(3), 2);
179        assert_eq!(phase_bits(4), 2);
180        assert_eq!(phase_bits(5), 3);
181    }
182
183    #[test]
184    fn single_movement_controller_is_valid_and_proven() {
185        let g = graph(1, &[]);
186        let plan = design_phase_plan(&g).unwrap();
187        let v = generate_controller(&g, &plan);
188        let ts = parse_transition_system(&v)
189            .unwrap_or_else(|e| panic!("single-movement controller did not parse: {}\n{v}", e.message));
190        assert_eq!(ts.prove_invariant(1), InductionOutcome::Proven, "{v}");
191    }
192
193    #[test]
194    fn six_movement_controller_is_proven() {
195        // A denser graph than the loop covers: 6 movements, two triangles sharing nothing.
196        let g = graph(6, &[(0, 1), (1, 2), (0, 2), (3, 4), (4, 5), (3, 5)]);
197        let plan = design_phase_plan(&g).unwrap();
198        assert_eq!(plan.num_phases, 3, "two disjoint triangles still need 3 phases");
199        let v = generate_controller(&g, &plan);
200        let ts = parse_transition_system(&v)
201            .unwrap_or_else(|e| panic!("did not parse: {}\n{v}", e.message));
202        assert_eq!(ts.prove_invariant(1), InductionOutcome::Proven, "{v}");
203    }
204}