1use logicaffeine_language::ast::logic::{LogicExpr, QuantifierKind, TemporalOperator, ThematicRole, Term};
15use logicaffeine_language::token::TokenType;
16use logicaffeine_language::Interner;
17
18#[derive(Debug)]
20pub struct SynthesizedSva {
21 pub sva_text: String,
23 pub body: String,
25 pub signals: Vec<String>,
27 pub kind: String,
29}
30
31pub fn synthesize_sva_from_spec(spec: &str, clock: &str) -> Result<SynthesizedSva, String> {
37 let mut last_err: Option<String> = None;
44 for block in spec_blocks(spec) {
45 if block.trim().is_empty() {
46 continue;
47 }
48 match synthesize_one(&block, clock) {
49 Ok(s) => return Ok(s),
50 Err(e) => last_err = Some(e),
51 }
52 }
53 Err(last_err.unwrap_or_else(|| {
54 "No hardware property found. Hardware specs are temporal sentences like \
55 \"Always, if request is high, then grant is high.\""
56 .to_string()
57 }))
58}
59
60fn spec_blocks(spec: &str) -> Vec<String> {
63 let mut blocks = Vec::new();
64 let mut cur = String::new();
65 for line in spec.lines() {
66 if line.trim_start().starts_with("##") {
67 if !cur.trim().is_empty() {
68 blocks.push(std::mem::take(&mut cur));
69 }
70 cur.clear();
71 } else {
72 cur.push_str(line);
73 cur.push('\n');
74 }
75 }
76 if !cur.trim().is_empty() {
77 blocks.push(cur);
78 }
79 if blocks.is_empty() {
80 blocks.push(spec.to_string());
81 }
82 blocks
83}
84
85fn synthesize_one(spec: &str, clock: &str) -> Result<SynthesizedSva, String> {
86 use logicaffeine_language::compile_kripke_with;
87 use logicaffeine_language::semantics::knowledge_graph::extract_from_kripke_ast;
88 use super::fol_to_verify::FolTranslator;
89 use super::sva_to_verify::extract_signal_names;
90
91 let (sva_body, signals, fol_signals) = compile_kripke_with(spec, |ast, interner| {
94 let mut fol_translator = FolTranslator::new(interner, 5);
96 let fol_result = fol_translator.translate_property(ast);
97 let fol_sigs = extract_signal_names(&fol_result);
98
99 let kg = extract_from_kripke_ast(ast, interner);
101 let kg_signals: Vec<String> = kg.signals.iter().map(|s| s.name.clone()).collect();
102
103 let body = synthesize_from_ast(ast, interner, clock, &fol_sigs);
105 (body, kg_signals, fol_sigs)
106 }).map_err(|_e| {
107 "not a hardware property — I couldn't read a temporal spec here. Try a sentence like \
108 \"Always, if request is high, then grant is high.\""
109 .to_string()
110 })?;
111
112 let body = sva_body;
113
114 if body.trim() == "0" {
117 return Err("Not a temporal property: this sentence describes an action or event, \
118 not a verifiable hardware property. Wrap in a temporal operator \
119 (e.g., \"Always, ...\") or restructure as a conditional.".to_string());
120 }
121
122 let trimmed = body.trim_start();
128 let is_reachability_cover = (trimmed.starts_with("s_eventually(") || trimmed.starts_with("cover"))
129 && !body.contains("|->")
130 && !body.contains("|=>");
131 let kind = if is_reachability_cover { "cover" } else { "assert" };
132
133 let sva_text = format!(
134 "{} property (@(posedge {}) {});",
135 kind, clock, body
136 );
137
138 Ok(SynthesizedSva {
139 sva_text,
140 body,
141 signals: if signals.is_empty() { fol_signals } else { signals },
142 kind: kind.to_string(),
143 })
144}
145
146fn synthesize_from_ast<'a>(
150 expr: &'a LogicExpr<'a>,
151 interner: &Interner,
152 clock: &str,
153 fol_signals: &[String],
154) -> String {
155 match expr {
156 LogicExpr::Temporal { operator, body } => {
158 let inner = synthesize_from_ast(body, interner, clock, fol_signals);
159 match operator {
160 TemporalOperator::Always => inner, TemporalOperator::Eventually => format!("s_eventually({})", inner),
162 TemporalOperator::Next => format!("nexttime({})", inner),
163 TemporalOperator::BoundedEventually(n) => format!("##[0:{}] {}", n, inner),
164 _ => inner,
165 }
166 }
167
168 LogicExpr::Quantifier { kind: QuantifierKind::Universal, body, variable, .. } => {
171 let var_name = interner.resolve(*variable).to_string();
172 if var_name.starts_with('w') {
173 if let LogicExpr::BinaryOp { left, right, op: TokenType::Implies } = body {
174 if is_accessibility_predicate(left, interner) {
175 let inner = synthesize_from_ast(right, interner, clock, fol_signals);
176 if is_next_temporal_predicate(left, interner) {
178 return format!("nexttime({})", inner);
179 }
180 return inner;
181 }
182 }
183 }
184 synthesize_from_ast(body, interner, clock, fol_signals)
186 }
187
188 LogicExpr::Quantifier { kind: QuantifierKind::Existential, body, variable, .. } => {
190 let var_name = interner.resolve(*variable).to_string();
191 if var_name.starts_with('w') {
192 if let LogicExpr::BinaryOp { left, right, op: TokenType::And } = body {
193 if is_accessibility_predicate(left, interner) {
194 return format!("s_eventually({})", synthesize_from_ast(right, interner, clock, fol_signals));
195 }
196 }
197 }
198 synthesize_from_ast(body, interner, clock, fol_signals)
199 }
200
201 LogicExpr::Quantifier { kind: QuantifierKind::AtMost(n), body, .. } => {
203 let inner = synthesize_from_ast(body, interner, clock, fol_signals);
204 if *n == 1 {
205 format!("$onehot0({})", inner)
206 } else {
207 format!("($countones({}) <= {})", inner, n)
208 }
209 }
210
211 LogicExpr::Quantifier { kind: QuantifierKind::AtLeast(n), body, .. } => {
212 let inner = synthesize_from_ast(body, interner, clock, fol_signals);
213 if *n == 1 {
214 inner } else {
216 format!("($countones({}) >= {})", inner, n)
217 }
218 }
219
220 LogicExpr::Quantifier { kind: QuantifierKind::Cardinal(n), body, .. } => {
221 let inner = synthesize_from_ast(body, interner, clock, fol_signals);
222 if *n == 1 {
223 format!("$onehot({})", inner)
224 } else {
225 format!("($countones({}) == {})", inner, n)
226 }
227 }
228
229 LogicExpr::Quantifier { body, .. } => {
231 synthesize_from_ast(body, interner, clock, fol_signals)
232 }
233
234 LogicExpr::BinaryOp { left, right, op: TokenType::If } => {
236 let ante = synthesize_from_ast(left, interner, clock, fol_signals);
237 let cons = synthesize_from_ast(right, interner, clock, fol_signals);
238 format!("{} |-> {}", ante, cons)
239 }
240
241 LogicExpr::BinaryOp { left, right, op: TokenType::Implies } => {
245 let ante = synthesize_from_ast(left, interner, clock, fol_signals);
246 let cons = synthesize_from_ast(right, interner, clock, fol_signals);
247 if ante == "1" {
249 cons
250 } else {
251 format!("(!({}) || ({}))", ante, cons)
252 }
253 }
254
255 LogicExpr::BinaryOp { left, right, op: TokenType::And } => {
257 let l = synthesize_from_ast(left, interner, clock, fol_signals);
258 let r = synthesize_from_ast(right, interner, clock, fol_signals);
259 format!("({} && {})", l, r)
260 }
261
262 LogicExpr::BinaryOp { left, right, op: TokenType::Or } => {
264 let l = synthesize_from_ast(left, interner, clock, fol_signals);
265 let r = synthesize_from_ast(right, interner, clock, fol_signals);
266 format!("({} || {})", l, r)
267 }
268
269 LogicExpr::UnaryOp { operand, .. } => {
271 let inner = synthesize_from_ast(operand, interner, clock, fol_signals);
272 format!("!({})", inner)
273 }
274
275 LogicExpr::Predicate { name, args, .. } => {
277 let pred_name = interner.resolve(*name).to_string();
278 if pred_name.contains("Accessible") || pred_name.contains("Reachable")
280 || pred_name.contains("Next_Temporal")
281 || pred_name == "Agent" || pred_name == "Theme"
282 {
283 return "1".to_string(); }
285 let arg_name = args.first().map(|a| term_to_string_helper(a, interner));
287 if let Some(ref arg) = arg_name {
288 let candidate = format!("{}_{}_", pred_name, arg);
289 if let Some(fol_sig) = fol_signals.iter().find(|s| {
290 s.to_lowercase() == candidate.to_lowercase()
291 }) {
292 return fol_sig.clone();
293 }
294 }
295 if let Some(fol_sig) = fol_signals.iter().find(|s| {
297 let s_lower = s.to_lowercase();
298 s_lower.contains(&pred_name.to_lowercase())
299 || pred_name.to_lowercase().contains(&s_lower)
300 }) {
301 fol_sig.clone()
302 } else {
303 pred_name.to_lowercase()
304 }
305 }
306
307 LogicExpr::NeoEvent(data) => {
309 let verb_name = interner.resolve(data.verb).to_string();
310 let agent_name = data.roles.iter()
311 .find(|(role, _)| matches!(role, ThematicRole::Agent))
312 .map(|(_, term)| term_to_string_helper(term, interner));
313
314 let candidate = if let Some(ref arg) = agent_name {
315 format!("{}_{}_", verb_name, arg)
316 } else {
317 verb_name.clone()
318 };
319
320 if let Some(fol_sig) = fol_signals.iter().find(|s| {
322 s.to_lowercase() == candidate.to_lowercase()
323 }) {
324 fol_sig.clone()
325 } else if let Some(fol_sig) = fol_signals.iter().find(|s| {
326 let s_lower = s.to_lowercase();
327 s_lower.contains(&verb_name.to_lowercase())
328 }) {
329 fol_sig.clone()
330 } else {
331 candidate
332 }
333 }
334
335 LogicExpr::TemporalBinary { operator, left, right } => {
337 let l = synthesize_from_ast(left, interner, clock, fol_signals);
338 let r = synthesize_from_ast(right, interner, clock, fol_signals);
339 use logicaffeine_language::ast::logic::BinaryTemporalOp;
340 match operator {
341 BinaryTemporalOp::Until => format!("({} until {})", l, r),
342 BinaryTemporalOp::Release => format!("({} release {})", l, r),
343 BinaryTemporalOp::WeakUntil => format!("({} weak_until {})", l, r),
344 }
345 }
346
347 LogicExpr::Modal { operand, .. } => {
349 synthesize_from_ast(operand, interner, clock, fol_signals)
350 }
351
352 LogicExpr::Aspectual { body, .. } => {
355 synthesize_from_ast(body, interner, clock, fol_signals)
356 }
357
358 LogicExpr::Voice { body, .. } => {
360 synthesize_from_ast(body, interner, clock, fol_signals)
361 }
362
363 LogicExpr::Relation(data) => {
365 let verb_name = interner.resolve(data.verb).to_string();
366 let subj_name = interner.resolve(data.subject.noun).to_string();
367 let obj_name = interner.resolve(data.object.noun).to_string();
368 let candidate = format!("{}_{}_", verb_name, subj_name);
369 if let Some(fol_sig) = fol_signals.iter().find(|s| {
370 s.to_lowercase() == candidate.to_lowercase()
371 }) {
372 fol_sig.clone()
373 } else if let Some(fol_sig) = fol_signals.iter().find(|s| {
374 let s_lower = s.to_lowercase();
375 s_lower.contains(&verb_name.to_lowercase())
376 || s_lower.contains(&subj_name.to_lowercase())
377 || s_lower.contains(&obj_name.to_lowercase())
378 }) {
379 fol_sig.clone()
380 } else {
381 format!("{}_{}_", verb_name, obj_name).to_lowercase()
382 }
383 }
384
385 LogicExpr::Categorical(data) => {
387 let subj_name = interner.resolve(data.subject.noun).to_string().to_lowercase();
388 let pred_name = interner.resolve(data.predicate.noun).to_string().to_lowercase();
389 if data.copula_negative {
390 format!("({} && !({}))", subj_name, pred_name)
391 } else {
392 format!("(!({}) || ({}))", subj_name, pred_name)
393 }
394 }
395
396 LogicExpr::Scopal { body, .. } => {
398 synthesize_from_ast(body, interner, clock, fol_signals)
399 }
400
401 LogicExpr::Causal { effect, cause } => {
403 let e = synthesize_from_ast(effect, interner, clock, fol_signals);
404 let c = synthesize_from_ast(cause, interner, clock, fol_signals);
405 format!("({} && {})", c, e)
406 }
407
408 LogicExpr::Concessive { main, .. } => {
410 synthesize_from_ast(main, interner, clock, fol_signals)
411 }
412
413 LogicExpr::Atom(sym) => {
415 let name = interner.resolve(*sym).to_string();
416 if let Some(fol_sig) = fol_signals.iter().find(|s| {
417 s.to_lowercase() == name.to_lowercase()
418 }) {
419 fol_sig.clone()
420 } else {
421 name.to_lowercase()
422 }
423 }
424
425 LogicExpr::Identity { left, right } => {
427 let l = term_to_string_helper(left, interner).to_lowercase();
428 let r = term_to_string_helper(right, interner).to_lowercase();
429 format!("({} == {})", l, r)
430 }
431
432 _ => "0".to_string(),
435 }
436}
437
438fn is_accessibility_predicate<'a>(expr: &'a LogicExpr<'a>, interner: &Interner) -> bool {
440 if let LogicExpr::Predicate { name, .. } = expr {
441 let pred_name = interner.resolve(*name).to_string();
442 pred_name.contains("Accessible") || pred_name.contains("Reachable") || pred_name.contains("Next_Temporal")
443 } else {
444 false
445 }
446}
447
448fn is_next_temporal_predicate<'a>(expr: &'a LogicExpr<'a>, interner: &Interner) -> bool {
450 if let LogicExpr::Predicate { name, .. } = expr {
451 let pred_name = interner.resolve(*name).to_string();
452 pred_name.contains("Next_Temporal")
453 } else {
454 false
455 }
456}
457
458fn term_to_string_helper<'a>(term: &'a Term<'a>, interner: &Interner) -> String {
460 match term {
461 Term::Constant(sym) | Term::Variable(sym) => interner.resolve(*sym).to_string(),
462 Term::Function(sym, _) => interner.resolve(*sym).to_string(),
463 _ => "unknown".to_string(),
464 }
465}
466
467#[cfg(test)]
468mod block_header_robustness {
469 use super::*;
470
471 #[test]
475 fn property_followed_by_a_block_synthesizes() {
476 let spec = "Always, if request then eventually grant.\n## Theorem t:\n It holds.";
477 let r = synthesize_sva_from_spec(spec, "clk");
478 assert!(r.is_ok(), "expected SVA, got error: {:?}", r.err());
479 assert!(r.unwrap().sva_text.contains("property"));
480 }
481
482 #[test]
485 fn property_inside_a_leading_block_still_works() {
486 let spec = "## Hardware\nAlways, if request then eventually grant.";
487 let r = synthesize_sva_from_spec(spec, "clk");
488 assert!(r.is_ok(), "expected SVA, got error: {:?}", r.err());
489 }
490}