1use logicaffeine_compile::codegen_sva::hw_pipeline::KgSummary;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum HardwareKind {
10 EnglishSpec,
12 SignalPlan,
14 Verilog,
16 RegisterAlloc,
18 Pigeonhole,
20}
21
22impl HardwareKind {
23 pub fn result_title(self) -> &'static str {
25 match self {
26 HardwareKind::EnglishSpec => "SVA",
27 HardwareKind::SignalPlan => "Signal Plan",
28 HardwareKind::Verilog => "Model Check",
29 HardwareKind::RegisterAlloc => "Allocation",
30 HardwareKind::Pigeonhole => "Proof",
31 }
32 }
33
34 pub fn compiles(self) -> bool {
37 match self {
38 HardwareKind::EnglishSpec => true,
39 HardwareKind::SignalPlan | HardwareKind::Verilog | HardwareKind::RegisterAlloc | HardwareKind::Pigeonhole => false,
40 }
41 }
42}
43
44pub fn kind_of(content: &str) -> Option<HardwareKind> {
48 use super::hardware_spec::{field_value, PIGEONS, REGISTERS};
49 if content.trim().is_empty() {
50 return None;
51 }
52 let names = |field: &str| content.lines().any(|line| field_value(line, field).is_some());
53 Some(if names(PIGEONS) {
54 HardwareKind::Pigeonhole
55 } else if names(REGISTERS) {
56 HardwareKind::RegisterAlloc
57 } else if content.contains("module") && content.contains("endmodule") {
58 HardwareKind::Verilog
59 } else if content.to_lowercase().contains("conflict") {
60 HardwareKind::SignalPlan
61 } else {
62 HardwareKind::EnglishSpec
63 })
64}
65
66pub fn is_verilog(content: &str) -> bool {
69 content.contains("endmodule")
70 || content.lines().any(|line| line.trim_start().strip_prefix("module").is_some_and(|rest| rest.starts_with([' ', '(', '#'])))
71}
72
73#[derive(Clone, Debug, PartialEq)]
75pub struct Section {
76 pub label: &'static str,
77 pub body: String,
78}
79
80#[derive(Clone, Debug, Default, PartialEq)]
82pub struct HardwareReport {
83 pub sections: Vec<Section>,
84 pub verdict: Option<(String, Option<bool>)>,
86 pub trace: Vec<(String, String)>,
88 pub signals: Vec<String>,
89 pub kg: KgSummary,
90 pub error: Option<String>,
91}
92
93pub fn analyze(content: &str) -> Option<(HardwareKind, HardwareReport)> {
95 let kind = kind_of(content)?;
96 let report = match kind {
97 HardwareKind::Pigeonhole => pigeonhole(content),
98 HardwareKind::RegisterAlloc => allocation(content),
99 HardwareKind::Verilog => verilog(content),
100 HardwareKind::SignalPlan => signal_plan(content),
101 HardwareKind::EnglishSpec => english_spec(content),
102 };
103 Some((kind, report))
104}
105
106fn failed(error: String) -> HardwareReport {
107 HardwareReport { error: Some(error), ..HardwareReport::default() }
108}
109
110fn spec_problem(content: &str) -> Option<String> {
112 super::hardware_spec::problems(content).into_iter().next().map(|p| p.message)
113}
114
115fn pigeonhole(content: &str) -> HardwareReport {
116 use super::pigeonhole_viz::{headline, parse_pigeonhole_spec, report};
117 match parse_pigeonhole_spec(content) {
118 Some(spec) => HardwareReport {
119 sections: vec![Section { label: "Certified pigeonhole refutation", body: report(&spec) }],
120 verdict: Some((headline(&spec), Some(true))),
121 ..HardwareReport::default()
122 },
123 None => failed(spec_problem(content).unwrap_or_else(|| "A pigeonhole spec is one line, `pigeons: N`.".to_string())),
124 }
125}
126
127fn allocation(content: &str) -> HardwareReport {
128 use super::register_alloc_viz::{allocation_report, linear_scan_assignment, parse_register_spec, render};
129 match parse_register_spec(content) {
130 Some(spec) if spec_problem(content).is_none() => {
131 let fits = linear_scan_assignment(&spec).iter().all(Option::is_some);
132 let (_, verdict) = render(&spec);
133 HardwareReport {
134 sections: vec![Section { label: "Certified register allocation", body: allocation_report(&spec) }],
135 verdict: Some((verdict, Some(fits))),
136 ..HardwareReport::default()
137 }
138 }
139 _ => failed(spec_problem(content).unwrap_or_else(|| {
140 "A register allocation needs at least one live range, `name: first-last`, under its `registers:` budget.".to_string()
141 })),
142 }
143}
144
145fn verilog(content: &str) -> HardwareReport {
146 use logicaffeine_compile::codegen_sva::rtl::parse_transition_system;
147 use logicaffeine_proof::bmc::{BmcOutcome, InductionOutcome};
148 let ts = match parse_transition_system(content) {
149 Ok(ts) => ts,
150 Err(e) => return failed(format!("RTL parse error: {}", e.message)),
151 };
152 let names: Vec<String> = ts
153 .registers
154 .iter()
155 .map(|r| if r.width > 1 { format!("{}[{}]", r.name, r.width) } else { r.name.clone() })
156 .collect();
157 let summary = format!(
158 "{} {}: {}.\nChecked in every reachable state by bounded model checking and k-induction, with no Z3.",
159 names.len(),
160 if names.len() == 1 { "register" } else { "registers" },
161 names.join(", ")
162 );
163 let to_trace = |trace: Vec<(String, bool)>| -> Vec<(String, String)> {
164 trace.into_iter().map(|(n, v)| (n, if v { "1" } else { "0" }.to_string())).collect()
165 };
166 let breaks = |k: u32| format!("\u{2717} Can break at step {k}: the waveform shows the run that breaks it");
167 let unexpressible = || ("This property isn't expressible yet. Try a simpler one.".to_string(), None);
168 let (verdict, trace) = match ts.prove_invariant(4) {
169 InductionOutcome::Proven => (
170 ("\u{2713} Always holds: proven for every reachable state (k-induction, no Z3)".to_string(), Some(true)),
171 ts.witness_trace(18).map(to_trace).unwrap_or_default(),
172 ),
173 InductionOutcome::CounterexampleAt { k, trace } => ((breaks(k), Some(false)), to_trace(trace)),
174 InductionOutcome::NotInductive => match ts.bmc(28) {
175 BmcOutcome::CounterexampleAt { k, trace } => ((breaks(k), Some(false)), to_trace(trace)),
176 BmcOutcome::NoneWithin(n) => (
177 (format!("No failure found within {n} steps, but this depth can't prove it always holds"), None),
178 Vec::new(),
179 ),
180 BmcOutcome::Unsupported => (unexpressible(), Vec::new()),
181 },
182 InductionOutcome::Unsupported => (unexpressible(), Vec::new()),
183 };
184 HardwareReport {
185 sections: vec![Section { label: "Transition system", body: summary }],
186 verdict: Some(verdict),
187 trace,
188 ..HardwareReport::default()
189 }
190}
191
192fn signal_plan(content: &str) -> HardwareReport {
193 use logicaffeine_compile::codegen_sva::controller_gen::generate_controller;
194 use logicaffeine_compile::codegen_sva::rtl::parse_transition_system;
195 use logicaffeine_compile::codegen_sva::signal_design::design_from_spec;
196 use logicaffeine_proof::bmc::InductionOutcome;
197 let (intersection, plan) = match design_from_spec(content) {
198 Ok(designed) => designed,
199 Err(e) => return failed(e),
200 };
201 let phases: Vec<String> = plan
202 .groups()
203 .iter()
204 .enumerate()
205 .map(|(p, group)| format!("Phase {}: {}", p + 1, intersection.names(group).join(", ")))
206 .collect();
207 let verilog = generate_controller(&intersection, &plan);
208 let controller_proven = matches!(parse_transition_system(&verilog).map(|ts| ts.prove_invariant(4)), Ok(InductionOutcome::Proven));
209 let minimal = if !plan.minimal_certified {
210 String::new()
211 } else if plan.num_phases <= 1 {
212 " \u{00b7} a single phase suffices".to_string()
213 } else {
214 format!(" \u{00b7} provably minimal ({}-phase impossible)", plan.num_phases - 1)
215 };
216 let controller_note = if controller_proven { " \u{00b7} generated controller PROVEN conflict-free (k-induction)" } else { "" };
217 HardwareReport {
218 sections: vec![
219 Section { label: "Phase plan", body: phases.join("\n") },
220 Section { label: "Generated controller", body: verilog.trim_end().to_string() },
221 ],
222 verdict: Some((
223 format!(
224 "\u{2713} Conflict-free {}-phase plan synthesized{minimal}{controller_note} (certified by our SAT solver, no Z3)",
225 plan.num_phases
226 ),
227 Some(true),
228 )),
229 signals: intersection.movements.clone(),
230 ..HardwareReport::default()
231 }
232}
233
234fn property(synth: &logicaffeine_compile::codegen_sva::fol_to_sva::SynthesizedSva) -> logicaffeine_compile::codegen_sva::SvaProperty {
236 use logicaffeine_compile::codegen_sva::{sanitize_property_name, SvaAssertionKind, SvaProperty};
237 let kind = match synth.kind.as_str() {
238 "cover" => SvaAssertionKind::Cover,
239 "assume" => SvaAssertionKind::Assume,
240 _ => SvaAssertionKind::Assert,
241 };
242 let name = if synth.signals.is_empty() { "p_property".to_string() } else { sanitize_property_name(&synth.signals.join("_")) };
243 SvaProperty { name, clock: "clk".to_string(), body: synth.body.clone(), kind }
244}
245
246fn english_spec(content: &str) -> HardwareReport {
247 use logicaffeine_compile::codegen_sva::fol_to_sva::synthesize_sva_from_spec;
248 use logicaffeine_compile::codegen_sva::hw_pipeline::{check_spec_vacuity, kg_summary, prove_spec_sva_equivalence, VacuityReport};
249 let synth = match synthesize_sva_from_spec(content, "clk") {
250 Ok(synth) => synth,
251 Err(e) => return failed(e),
252 };
253 let psl = logicaffeine_compile::codegen_sva::emit_psl_property(&property(&synth));
254 let vacuity = match check_spec_vacuity(content, 8) {
255 Ok(VacuityReport::Vacuous) => " \u{00b7} \u{26A0} never triggers (vacuous)",
256 Ok(VacuityReport::NonVacuous) => " \u{00b7} the trigger can actually fire",
257 _ => "",
258 };
259 let (verdict, trace) = match prove_spec_sva_equivalence(content, &synth.body, 8) {
260 Ok(r) if r.equivalent => ((format!("\u{2713} Matches the spec: certified, kernel-checked, no Z3{vacuity}"), Some(true)), Vec::new()),
261 Ok(r) => {
262 let trace = r.counterexample.unwrap_or_default();
263 let shown = trace.iter().map(|(k, v)| format!("{k}={v}")).collect::<Vec<_>>().join(", ");
264 ((format!("\u{2717} Not equivalent to the spec. Counterexample: {shown}{vacuity}"), Some(false)), trace)
265 }
266 Err(e) => ((format!("Proof unavailable: {e}{vacuity}"), None), Vec::new()),
267 };
268 HardwareReport {
269 sections: vec![
270 Section { label: "SystemVerilog Assertion", body: synth.sva_text.clone() },
271 Section { label: "PSL", body: psl },
272 ],
273 verdict: Some(verdict),
274 trace,
275 signals: synth.signals.clone(),
276 kg: kg_summary(content).unwrap_or_default(),
277 error: None,
278 }
279}
280
281#[derive(Clone, Debug, PartialEq)]
283pub enum Compiled {
284 Rust(String),
286 Nothing(&'static str),
288 Failed(String),
290}
291
292pub fn nothing_to_compile(kind: HardwareKind) -> Option<&'static str> {
295 match kind {
296 HardwareKind::EnglishSpec => None,
297 HardwareKind::SignalPlan => Some(
298 "A signal plan compiles to hardware, not Rust: its generated controller is the Verilog in the Signal Plan panel, proven conflict-free.",
299 ),
300 HardwareKind::Verilog => Some(
301 "Verilog is already hardware, so there is no Rust monitor to generate. Execute model-checks its assertion.",
302 ),
303 HardwareKind::RegisterAlloc => Some("A register allocation has nothing to compile. Execute certifies it."),
304 HardwareKind::Pigeonhole => Some("A pigeonhole instance has nothing to compile. Execute certifies its refutation."),
305 }
306}
307
308pub fn compiled(content: &str) -> Compiled {
311 let Some(kind) = kind_of(content) else { return Compiled::Nothing("Write a design to compile.") };
312 if let Some(note) = nothing_to_compile(kind) {
313 return Compiled::Nothing(note);
314 }
315 match logicaffeine_compile::codegen_sva::fol_to_sva::synthesize_sva_from_spec(content, "clk") {
316 Ok(synth) => Compiled::Rust(logicaffeine_compile::codegen_sva::emit_rust_monitor(&property(&synth))),
317 Err(e) => Compiled::Failed(e),
318 }
319}
320
321#[cfg(test)]
322#[path = "../../../../../crates/logicaffeine_tests/unit/logicaffeine_web/ui/pages/hardware_report/tests.rs"]
323mod tests;