1use super::sva_model::{SvaExpr, parse_sva, sva_expr_to_string, sva_exprs_structurally_equivalent};
7use super::sva_to_verify::{SvaTranslator, BoundedExpr, TranslateResult};
8use super::fol_to_verify::FolTranslator;
9use super::{SvaProperty, SvaAssertionKind, emit_sva_property, sanitize_property_name};
10use logicaffeine_language::semantics::knowledge_graph::{HwKnowledgeGraph, SignalRole};
11use std::collections::HashMap;
12
13#[derive(Debug, Clone)]
23pub struct SignalMap {
24 map: HashMap<String, String>,
25}
26
27impl SignalMap {
28 pub fn new() -> Self {
29 Self { map: HashMap::new() }
30 }
31
32 pub fn add(&mut self, fol_arg: &str, sva_signal: &str) {
34 self.map.insert(fol_arg.to_string(), sva_signal.to_string());
35 }
36
37 pub fn resolve(&self, fol_arg: &str) -> Option<&str> {
39 self.map.get(fol_arg).map(|s| s.as_str())
40 }
41
42 pub fn from_decls(decls: &[HwSignalDecl]) -> Self {
44 let mut map = Self::new();
45 for decl in decls {
46 map.add(&decl.english_name, &decl.sva_name);
47 }
48 map
49 }
50}
51
52#[derive(Debug, Clone)]
62pub struct HwSignalDecl {
63 pub english_name: String,
64 pub sva_name: String,
65 pub width: u32,
66 pub role: SignalRole,
67}
68
69impl HwSignalDecl {
70 pub fn new(english_name: &str, sva_name: &str, width: u32, role: SignalRole) -> Self {
71 Self {
72 english_name: english_name.to_string(),
73 sva_name: sva_name.to_string(),
74 width,
75 role,
76 }
77 }
78}
79
80#[derive(Debug)]
86pub struct EquivalenceResult {
87 pub equivalent: bool,
89 pub counterexample: Option<Vec<(String, String)>>,
91 pub bound: u32,
93}
94
95#[derive(Debug)]
97pub struct HwSpec {
98 pub fol_text: String,
100 pub kg: HwKnowledgeGraph,
102}
103
104#[derive(Debug)]
106pub struct PipelineResult {
107 pub property_name: String,
109 pub result: EquivalenceResult,
111 pub sva_text: String,
113 pub fol_text: String,
115}
116
117#[derive(Debug)]
119pub enum HwError {
120 ParseError(String),
121 SvaParseError(String),
122 VerificationError(String),
123}
124
125impl std::fmt::Display for HwError {
126 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127 match self {
128 HwError::ParseError(msg) => write!(f, "Parse error: {}", msg),
129 HwError::SvaParseError(msg) => write!(f, "SVA parse error: {}", msg),
130 HwError::VerificationError(msg) => write!(f, "Verification error: {}", msg),
131 }
132 }
133}
134
135pub fn check_structural_equivalence(sva_a: &str, sva_b: &str) -> Result<bool, HwError> {
141 let expr_a = parse_sva(sva_a).map_err(|e| HwError::SvaParseError(e.message))?;
142 let expr_b = parse_sva(sva_b).map_err(|e| HwError::SvaParseError(e.message))?;
143 Ok(sva_exprs_structurally_equivalent(&expr_a, &expr_b))
144}
145
146pub fn check_bounded_equivalence(
148 fol_bounded: &BoundedExpr,
149 sva_bounded: &BoundedExpr,
150 bound: u32,
151) -> EquivalenceResult {
152 let equivalent = bounded_exprs_equal(fol_bounded, sva_bounded);
153 EquivalenceResult {
154 equivalent,
155 counterexample: None,
156 bound,
157 }
158}
159
160pub fn translate_sva_to_bounded(sva_text: &str, bound: u32) -> Result<TranslateResult, HwError> {
164 let sva_expr = parse_sva(sva_text).map_err(|e| HwError::SvaParseError(e.message))?;
165 let mut translator = SvaTranslator::new(bound);
166 let result = translator.translate_property(&sva_expr);
167 Ok(result)
168}
169
170fn translate_sva_for_equiv(sva_text: &str, bound: u32) -> Result<TranslateResult, HwError> {
174 let sva_expr = parse_sva(sva_text).map_err(|e| HwError::SvaParseError(e.message))?;
175 let mut translator = SvaTranslator::new(bound);
176 if sva_has_outermost_temporal(&sva_expr) {
177 let expr = translator.translate(&sva_expr, 0);
178 let declarations: Vec<String> = translator.declarations.iter().cloned().collect();
179 Ok(TranslateResult { expr, declarations })
180 } else {
181 Ok(translator.translate_property(&sva_expr))
182 }
183}
184
185fn sva_has_outermost_temporal(expr: &SvaExpr) -> bool {
189 matches!(expr, SvaExpr::SEventually(_) | SvaExpr::Nexttime(_, _) | SvaExpr::SAlways(_))
190}
191
192pub fn translate_spec_to_bounded(
194 spec: &str,
195 bound: u32,
196) -> Result<TranslateResult, HwError> {
197 logicaffeine_language::compile_kripke_with(spec, |ast, interner| {
198 let mut translator = FolTranslator::new(interner, bound);
199 translator.translate_property(ast)
200 })
201 .map_err(|e| HwError::ParseError(format!("{:?}", e)))
202}
203
204pub fn compile_hw_property(
210 spec: &str,
211 decls: &[HwSignalDecl],
212 bound: u32,
213) -> Result<TranslateResult, HwError> {
214 let signal_map = SignalMap::from_decls(decls);
215 logicaffeine_language::compile_kripke_with(spec, |ast, interner| {
216 let mut translator = FolTranslator::new(interner, bound);
217 translator.set_signal_map(&signal_map);
218 translator.translate_property(ast)
219 })
220 .map_err(|e| HwError::ParseError(format!("{:?}", e)))
221}
222
223pub fn compile_hw_spec(source: &str) -> Result<String, HwError> {
225 logicaffeine_language::compile_kripke(source)
226 .map_err(|e| HwError::ParseError(format!("{:?}", e)))
227}
228
229pub fn emit_hw_sva(name: &str, clock: &str, body: &str, kind: SvaAssertionKind) -> String {
231 let prop = SvaProperty {
232 name: sanitize_property_name(name),
233 clock: clock.to_string(),
234 body: body.to_string(),
235 kind,
236 };
237 emit_sva_property(&prop)
238}
239
240pub fn extract_kg(spec: &str) -> Result<HwKnowledgeGraph, HwError> {
242 logicaffeine_language::compile_kripke_with(spec, |ast, interner| {
243 logicaffeine_language::semantics::knowledge_graph::extract_from_kripke_ast(ast, interner)
244 })
245 .map_err(|e| HwError::ParseError(format!("{:?}", e)))
246}
247
248#[cfg(feature = "verification")]
252pub fn check_z3_equivalence(
253 spec_source: &str,
254 sva_text: &str,
255 bound: u32,
256) -> Result<logicaffeine_verify::equivalence::EquivalenceResult, HwError> {
257 use super::sva_to_verify::{bounded_to_verify, extract_signal_names};
258
259 let spec_bounded = translate_spec_to_bounded(spec_source, bound)?;
260 let spec_verify = bounded_to_verify(&spec_bounded.expr);
261
262 let sva_bounded = translate_sva_for_equiv(sva_text, bound)?;
263 let sva_verify = bounded_to_verify(&sva_bounded.expr);
264
265 let mut all_signals = extract_signal_names(&spec_bounded);
266 let sva_signals = extract_signal_names(&sva_bounded);
267 for sig in sva_signals {
268 if !all_signals.contains(&sig) {
269 all_signals.push(sig);
270 }
271 }
272
273 Ok(logicaffeine_verify::equivalence::check_equivalence(
274 &spec_verify, &sva_verify, &all_signals, bound as usize,
275 ))
276}
277
278#[cfg(feature = "verification")]
283pub fn check_z3_hw_equivalence(
284 spec: &str,
285 sva_text: &str,
286 decls: &[HwSignalDecl],
287 bound: u32,
288) -> Result<logicaffeine_verify::equivalence::EquivalenceResult, HwError> {
289 use super::sva_to_verify::{bounded_to_verify, extract_signal_names};
290
291 let spec_bounded = compile_hw_property(spec, decls, bound)?;
293 let spec_verify = bounded_to_verify(&spec_bounded.expr);
294
295 let sva_bounded = translate_sva_for_equiv(sva_text, bound)?;
297 let sva_verify = bounded_to_verify(&sva_bounded.expr);
298
299 let mut all_signals: Vec<String> = decls.iter().map(|d| d.sva_name.clone()).collect();
301 let spec_signals = extract_signal_names(&spec_bounded);
302 let sva_signals = extract_signal_names(&sva_bounded);
303 for sig in spec_signals.into_iter().chain(sva_signals.into_iter()) {
304 if !all_signals.contains(&sig) {
305 all_signals.push(sig);
306 }
307 }
308
309 Ok(logicaffeine_verify::equivalence::check_equivalence(
311 &spec_verify, &sva_verify, &all_signals, bound as usize,
312 ))
313}
314
315pub fn prove_bounded_equivalence(
323 fol_bounded: &BoundedExpr,
324 sva_bounded: &BoundedExpr,
325 bound: u32,
326) -> Result<EquivalenceResult, HwError> {
327 use super::sva_to_proof::bounded_to_proof;
328 use logicaffeine_proof::sat::{prove_equivalence, EquivOutcome};
329
330 let fol = bounded_to_proof(fol_bounded).ok_or_else(|| {
331 HwError::VerificationError(
332 "FOL obligation is outside the Boolean fragment (bit-blasting not yet wired)".into(),
333 )
334 })?;
335 let sva = bounded_to_proof(sva_bounded).ok_or_else(|| {
336 HwError::VerificationError(
337 "SVA obligation is outside the Boolean fragment (bit-blasting not yet wired)".into(),
338 )
339 })?;
340
341 match prove_equivalence(&fol, &sva) {
342 EquivOutcome::Equivalent => Ok(EquivalenceResult {
343 equivalent: true,
344 counterexample: None,
345 bound,
346 }),
347 EquivOutcome::Differ(model) => Ok(EquivalenceResult {
348 equivalent: false,
349 counterexample: Some(
350 model
351 .into_iter()
352 .map(|(name, v)| (name, if v { "1" } else { "0" }.to_string()))
353 .collect(),
354 ),
355 bound,
356 }),
357 EquivOutcome::Unsupported => Err(HwError::VerificationError(
358 "obligation not purely propositional — escalate to bit-blasting".into(),
359 )),
360 }
361}
362
363pub fn prove_spec_sva_equivalence(
367 spec_source: &str,
368 sva_text: &str,
369 bound: u32,
370) -> Result<EquivalenceResult, HwError> {
371 let spec_bounded = translate_spec_to_bounded(spec_source, bound)?;
372 let sva_bounded = translate_sva_for_equiv(sva_text, bound)?;
373 prove_bounded_equivalence(&spec_bounded.expr, &sva_bounded.expr, bound)
374}
375
376#[derive(Debug, Clone, PartialEq, Eq)]
378pub struct KgNode {
379 pub name: String,
380 pub role: String,
381 pub width: u32,
382}
383
384#[derive(Debug, Clone, PartialEq, Eq)]
386pub struct KgLink {
387 pub from: usize,
388 pub to: usize,
389 pub relation: String,
390}
391
392#[derive(Debug, Clone, PartialEq, Eq, Default)]
395pub struct KgSummary {
396 pub nodes: Vec<KgNode>,
397 pub links: Vec<KgLink>,
398}
399
400pub fn kg_summary(spec: &str) -> Result<KgSummary, HwError> {
403 use logicaffeine_language::semantics::knowledge_graph::{KgRelation, SignalRole};
404 use std::collections::HashMap;
405
406 let kg = extract_kg(spec)?;
407 let mut nodes = Vec::new();
408 let mut index: HashMap<String, usize> = HashMap::new();
409 for s in &kg.signals {
410 let role = match s.role {
411 SignalRole::Input => "input",
412 SignalRole::Output => "output",
413 SignalRole::Internal => "internal",
414 SignalRole::Clock => "clock",
415 }
416 .to_string();
417 index.insert(s.name.clone(), nodes.len());
418 nodes.push(KgNode { name: s.name.clone(), role, width: s.width });
419 }
420
421 let mut links = Vec::new();
422 let mut seen = std::collections::HashSet::new();
423 let mut add = |from: &str, to: &str, rel: String, links: &mut Vec<KgLink>| {
424 if let (Some(&f), Some(&t)) = (index.get(from), index.get(to)) {
425 if f != t && seen.insert((f, t, rel.clone())) {
426 links.push(KgLink { from: f, to: t, relation: rel });
427 }
428 }
429 };
430 for e in &kg.edges {
431 let rel = match e.relation {
432 KgRelation::Temporal => "temporal",
433 KgRelation::Triggers => "triggers",
434 KgRelation::Constrains => "constrains",
435 KgRelation::TypeOf => "type",
436 }
437 .to_string();
438 add(&e.from, &e.to, rel, &mut links);
439 }
440 for (from, to, rel) in &kg.typed_edges {
441 add(from, to, typed_relation_label(rel), &mut links);
442 }
443
444 Ok(KgSummary { nodes, links })
445}
446
447fn typed_relation_label(rel: &logicaffeine_language::semantics::knowledge_graph::HwRelation) -> String {
448 use logicaffeine_language::semantics::knowledge_graph::HwRelation as R;
449 match rel {
450 R::Drives | R::DrivesRegistered { .. } => "drives",
451 R::DataFlow => "data",
452 R::Reads => "reads",
453 R::Writes => "writes",
454 R::Controls => "controls",
455 R::Selects => "selects",
456 R::Enables => "enables",
457 R::Resets => "resets",
458 R::Triggers { .. } => "triggers",
459 R::Constrains => "constrains",
460 R::Follows { .. } => "follows",
461 R::Precedes => "precedes",
462 R::Preserves => "preserves",
463 R::Contains => "contains",
464 R::Instantiates => "instantiates",
465 R::ConnectsTo => "connects",
466 R::BelongsToDomain { .. } => "domain",
467 R::HandshakesWith => "handshake",
468 R::Acknowledges => "acks",
469 R::Pipelines { .. } => "pipeline",
470 R::MutuallyExcludes => "mutex",
471 R::EventuallyFollows => "eventually",
472 R::AssumedBy => "assumed",
473 }
474 .to_string()
475}
476
477#[derive(Debug, Clone, PartialEq, Eq)]
481pub struct WaveSignal {
482 pub name: String,
483 pub width: u32,
484 pub values: Vec<Option<u64>>,
485}
486
487#[derive(Debug, Clone, PartialEq, Eq, Default)]
489pub struct Waveform {
490 pub timesteps: u32,
491 pub signals: Vec<WaveSignal>,
492}
493
494pub fn counterexample_waveform(counterexample: &[(String, String)]) -> Waveform {
499 use std::collections::BTreeMap;
500 struct Acc {
502 width: u32,
503 per_t: BTreeMap<u32, BTreeMap<u32, bool>>,
504 }
505 let mut sigs: BTreeMap<String, Acc> = BTreeMap::new();
506 let mut max_t = 0u32;
507 let mut any = false;
508 for (key, val) in counterexample {
509 let bit_val = match val.as_str() {
510 "1" | "true" => true,
511 "0" | "false" => false,
512 _ => continue,
513 };
514 let (base, bit, multibit) = match key.rsplit_once('#') {
516 Some((b, idx)) => match idx.parse::<u32>() {
517 Ok(i) => (b, i, true),
518 Err(_) => continue,
519 },
520 None => (key.as_str(), 0u32, false),
521 };
522 let Some((name, t_str)) = base.rsplit_once('@') else {
523 continue;
524 };
525 let Ok(t) = t_str.parse::<u32>() else {
526 continue;
527 };
528 let acc = sigs.entry(name.to_string()).or_insert(Acc { width: 1, per_t: BTreeMap::new() });
529 if multibit {
530 acc.width = acc.width.max(bit + 1);
531 }
532 acc.per_t.entry(t).or_default().insert(bit, bit_val);
533 max_t = max_t.max(t);
534 any = true;
535 }
536 let timesteps = if any { max_t + 1 } else { 0 };
537 let signals = sigs
538 .into_iter()
539 .map(|(name, acc)| {
540 let values = (0..timesteps)
541 .map(|t| {
542 acc.per_t.get(&t).map(|bits| {
543 bits.iter().fold(0u64, |v, (&i, &b)| if b { v | (1u64 << i) } else { v })
544 })
545 })
546 .collect();
547 WaveSignal { name, width: acc.width, values }
548 })
549 .collect();
550 Waveform { timesteps, signals }
551}
552
553#[derive(Debug, Clone, PartialEq, Eq)]
555pub enum VacuityReport {
556 NonVacuous,
558 Vacuous,
560 NoTrigger,
562 Unsupported,
564}
565
566fn first_antecedent(e: &BoundedExpr) -> Option<&BoundedExpr> {
569 match e {
570 BoundedExpr::Implies(a, _) => Some(a),
571 BoundedExpr::And(l, r) | BoundedExpr::Or(l, r) => {
572 first_antecedent(l).or_else(|| first_antecedent(r))
573 }
574 BoundedExpr::Not(x) => first_antecedent(x),
575 _ => None,
576 }
577}
578
579pub fn check_spec_vacuity(spec_source: &str, bound: u32) -> Result<VacuityReport, HwError> {
583 use super::sva_to_proof::bounded_to_proof;
584 use logicaffeine_proof::bmc::{check_vacuity, VacuityOutcome};
585
586 let fol = translate_spec_to_bounded(spec_source, bound)?;
587 let antecedent = match first_antecedent(&fol.expr) {
588 Some(a) => a,
589 None => return Ok(VacuityReport::NoTrigger),
590 };
591 let proof = match bounded_to_proof(antecedent) {
592 Some(p) => p,
593 None => return Ok(VacuityReport::Unsupported),
594 };
595 Ok(match check_vacuity(&proof) {
596 VacuityOutcome::Vacuous => VacuityReport::Vacuous,
597 VacuityOutcome::Reachable(_) => VacuityReport::NonVacuous,
598 VacuityOutcome::Unsupported => VacuityReport::Unsupported,
599 })
600}
601
602#[cfg(test)]
603mod native_prove_tests {
604 use super::*;
605
606 #[test]
608 fn sva_self_equivalence_is_certified() {
609 let a = translate_sva_to_bounded("!(grant_a && grant_b)", 3).unwrap();
610 let b = translate_sva_to_bounded("!(grant_a && grant_b)", 3).unwrap();
611 let r = prove_bounded_equivalence(&a.expr, &b.expr, 3).unwrap();
612 assert!(r.equivalent, "self-equivalence must hold");
613 assert!(r.counterexample.is_none());
614 }
615
616 #[test]
620 fn de_morgan_is_semantically_equivalent() {
621 let a = translate_sva_to_bounded("!(grant_a && grant_b)", 3).unwrap();
622 let b = translate_sva_to_bounded("!grant_a || !grant_b", 3).unwrap();
623 assert!(
624 !check_bounded_equivalence(&a.expr, &b.expr, 3).equivalent,
625 "structural equality should NOT see De Morgan duals as equal"
626 );
627 let r = prove_bounded_equivalence(&a.expr, &b.expr, 3).unwrap();
628 assert!(r.equivalent, "De Morgan duals must be semantically equivalent");
629 }
630
631 #[test]
634 fn synthesized_sva_is_equivalent_to_its_spec() {
635 let spec = "Always, if request is high, then acknowledge is high.";
636 let synth = crate::codegen_sva::fol_to_sva::synthesize_sva_from_spec(spec, "clk").unwrap();
637 let r = prove_spec_sva_equivalence(spec, &synth.body, 3).unwrap();
638 assert!(r.equivalent, "synthesis must preserve meaning; got {:?}", r);
639 }
640
641 #[test]
643 fn distinct_properties_differ_with_counterexample() {
644 let a = translate_sva_to_bounded("!(grant_a && grant_b)", 2).unwrap();
645 let b = translate_sva_to_bounded("grant_a |-> grant_b", 2).unwrap();
646 let r = prove_bounded_equivalence(&a.expr, &b.expr, 2).unwrap();
647 assert!(!r.equivalent, "a mutex and an implication are not equivalent");
648 let ce = r.counterexample.expect("a counterexample trace");
649 assert!(!ce.is_empty(), "counterexample must bind some signal@t");
650 }
651
652 #[test]
653 fn implication_spec_has_a_reachable_trigger() {
654 let r = check_spec_vacuity("Always, if request is high, then acknowledge is high.", 3)
657 .unwrap();
658 assert_eq!(r, VacuityReport::NonVacuous);
659 }
660
661 #[test]
662 fn kg_summary_extracts_signals_and_relations() {
663 let kg = kg_summary("Always, if request is high, then acknowledge is high.")
664 .expect("extracts a knowledge graph");
665 assert!(!kg.nodes.is_empty(), "the spec mentions signals: {kg:?}");
666 for l in &kg.links {
668 assert!(l.from < kg.nodes.len() && l.to < kg.nodes.len(), "dangling link {l:?}");
669 assert!(!l.relation.is_empty());
670 }
671 let names: Vec<&str> = kg.nodes.iter().map(|n| n.name.as_str()).collect();
673 assert!(
674 names.iter().any(|n| n.eq_ignore_ascii_case("request")),
675 "expected a Request signal among {names:?}"
676 );
677 assert!(
678 names.iter().any(|n| n.eq_ignore_ascii_case("acknowledge")),
679 "expected an Acknowledge signal among {names:?}"
680 );
681 }
682
683 #[test]
684 fn waveform_groups_boolean_signals_over_timesteps() {
685 let ce = vec![
686 ("req@0".to_string(), "0".to_string()),
687 ("req@1".to_string(), "1".to_string()),
688 ("ack@1".to_string(), "0".to_string()),
689 ];
690 let wf = counterexample_waveform(&ce);
691 assert_eq!(wf.timesteps, 2);
692 assert_eq!(wf.signals.len(), 2);
693 let ack = wf.signals.iter().find(|s| s.name == "ack").unwrap();
694 assert_eq!(ack.width, 1);
695 assert_eq!(ack.values, vec![None, Some(0)]);
696 let req = wf.signals.iter().find(|s| s.name == "req").unwrap();
697 assert_eq!(req.values, vec![Some(0), Some(1)]);
698 }
699
700 #[test]
701 fn waveform_reconstructs_multibit_register_values() {
702 let ce = vec![
704 ("cnt@0#0".to_string(), "1".to_string()),
705 ("cnt@0#1".to_string(), "0".to_string()),
706 ("cnt@1#0".to_string(), "0".to_string()),
707 ("cnt@1#1".to_string(), "1".to_string()),
708 ];
709 let wf = counterexample_waveform(&ce);
710 let cnt = wf.signals.iter().find(|s| s.name == "cnt").unwrap();
711 assert_eq!(cnt.width, 2);
712 assert_eq!(cnt.values, vec![Some(1), Some(2)]);
713 }
714
715 #[test]
716 fn waveform_skips_malformed_keys() {
717 let ce = vec![("nonsense".to_string(), "1".to_string())];
718 assert_eq!(counterexample_waveform(&ce), Waveform::default());
719 }
720
721 #[test]
722 fn first_antecedent_extracts_the_trigger() {
723 use crate::codegen_sva::sva_to_verify::BoundedExpr;
724 let v = |s: &str| BoundedExpr::Var(s.to_string());
725 let bx = |e: BoundedExpr| Box::new(e);
726 let prop = BoundedExpr::And(
728 bx(BoundedExpr::Implies(bx(v("req@0")), bx(v("ack@0")))),
729 bx(BoundedExpr::Implies(bx(v("req@1")), bx(v("ack@1")))),
730 );
731 assert_eq!(first_antecedent(&prop), Some(&v("req@0")));
732 let safety = BoundedExpr::Not(bx(BoundedExpr::And(bx(v("a@0")), bx(v("b@0")))));
734 assert_eq!(first_antecedent(&safety), None);
735 }
736}
737
738fn bounded_exprs_equal(a: &BoundedExpr, b: &BoundedExpr) -> bool {
740 match (a, b) {
741 (BoundedExpr::Var(va), BoundedExpr::Var(vb)) => va == vb,
742 (BoundedExpr::Bool(va), BoundedExpr::Bool(vb)) => va == vb,
743 (BoundedExpr::Int(va), BoundedExpr::Int(vb)) => va == vb,
744 (BoundedExpr::And(la, ra), BoundedExpr::And(lb, rb)) => {
745 bounded_exprs_equal(la, lb) && bounded_exprs_equal(ra, rb)
746 }
747 (BoundedExpr::Or(la, ra), BoundedExpr::Or(lb, rb)) => {
748 bounded_exprs_equal(la, lb) && bounded_exprs_equal(ra, rb)
749 }
750 (BoundedExpr::Not(ia), BoundedExpr::Not(ib)) => bounded_exprs_equal(ia, ib),
751 (BoundedExpr::Implies(la, ra), BoundedExpr::Implies(lb, rb)) => {
752 bounded_exprs_equal(la, lb) && bounded_exprs_equal(ra, rb)
753 }
754 (BoundedExpr::Eq(la, ra), BoundedExpr::Eq(lb, rb)) => {
755 bounded_exprs_equal(la, lb) && bounded_exprs_equal(ra, rb)
756 }
757 (BoundedExpr::Lt(la, ra), BoundedExpr::Lt(lb, rb)) => {
758 bounded_exprs_equal(la, lb) && bounded_exprs_equal(ra, rb)
759 }
760 (BoundedExpr::Gt(la, ra), BoundedExpr::Gt(lb, rb)) => {
761 bounded_exprs_equal(la, lb) && bounded_exprs_equal(ra, rb)
762 }
763 (BoundedExpr::Lte(la, ra), BoundedExpr::Lte(lb, rb)) => {
764 bounded_exprs_equal(la, lb) && bounded_exprs_equal(ra, rb)
765 }
766 (BoundedExpr::Gte(la, ra), BoundedExpr::Gte(lb, rb)) => {
767 bounded_exprs_equal(la, lb) && bounded_exprs_equal(ra, rb)
768 }
769 _ => false,
770 }
771}