Skip to main content

tensorlogic_ir/expr/
ltl_ctl_utilities.rs

1//! LTL/CTL temporal logic utilities and model checking support.
2//!
3//! This module provides advanced temporal logic functionality including:
4//! - Formula classification (safety, liveness, fairness properties)
5//! - Temporal pattern recognition and analysis
6//! - Model checking preparation and utilities
7//! - Extended temporal equivalences beyond basic optimizations
8//! - Temporal formula complexity analysis
9
10use super::TLExpr;
11use std::collections::HashSet;
12
13/// Classification of temporal formulas.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum TemporalClass {
16    /// Safety property: "something bad never happens"
17    Safety,
18    /// Liveness property: "something good eventually happens"
19    Liveness,
20    /// Fairness property: "if requested infinitely often, granted infinitely often"
21    Fairness,
22    /// Persistence property: "eventually always true"
23    Persistence,
24    /// Recurrence property: "infinitely often true"
25    Recurrence,
26    /// Mixed: combination of multiple property types
27    Mixed,
28}
29
30/// Temporal pattern types.
31#[derive(Debug, Clone, PartialEq, Eq, Hash)]
32pub enum TemporalPattern {
33    /// Always P: safety property
34    AlwaysP,
35    /// Eventually P: liveness property
36    EventuallyP,
37    /// Eventually Always P: persistence
38    EventuallyAlwaysP,
39    /// Always Eventually P: recurrence
40    AlwaysEventuallyP,
41    /// P Until Q: reachability
42    PUntilQ,
43    /// Always (P implies Eventually Q): response property
44    AlwaysImpliesEventually,
45    /// Always (P implies Next Q): immediate response
46    AlwaysImpliesNext,
47    /// Nested temporal operators
48    Complex,
49}
50
51/// Temporal logic complexity metrics.
52#[derive(Debug, Clone, Default)]
53pub struct TemporalComplexity {
54    /// Maximum nesting depth of temporal operators
55    pub temporal_depth: usize,
56    /// Number of temporal operators
57    pub temporal_op_count: usize,
58    /// Number of Until operators (most expensive)
59    pub until_count: usize,
60    /// Number of Release operators
61    pub release_count: usize,
62    /// Number of Next operators (least expensive)
63    pub next_count: usize,
64    /// Whether formula contains fairness constraints
65    pub has_fairness: bool,
66}
67
68/// Classify a temporal formula.
69///
70/// Determines whether the formula expresses a safety, liveness, fairness,
71/// or other temporal property.
72pub fn classify_temporal_formula(expr: &TLExpr) -> TemporalClass {
73    match expr {
74        // Eventually Always P is persistence (check before general Eventually)
75        TLExpr::Eventually(e) if matches!(**e, TLExpr::Always(_)) => TemporalClass::Persistence,
76
77        // Eventually P is a liveness property
78        TLExpr::Eventually(_) => TemporalClass::Liveness,
79
80        // Always Eventually P is recurrence (check before general Always)
81        TLExpr::Always(e) if matches!(**e, TLExpr::Eventually(_)) => TemporalClass::Recurrence,
82
83        // Fairness: Always (P -> Eventually Q) (check before general Always)
84        TLExpr::Always(e) if matches!(&**e, TLExpr::Imply(_, then_branch) if matches!(**then_branch, TLExpr::Eventually(_))) => {
85            TemporalClass::Fairness
86        }
87
88        // Always P is a safety property (general case)
89        TLExpr::Always(_) => TemporalClass::Safety,
90
91        // Conjunction of temporal properties
92        TLExpr::And(left, right) => {
93            let left_class = classify_temporal_formula(left);
94            let right_class = classify_temporal_formula(right);
95            if left_class == right_class {
96                left_class
97            } else {
98                TemporalClass::Mixed
99            }
100        }
101
102        _ => TemporalClass::Mixed,
103    }
104}
105
106/// Identify temporal pattern in a formula.
107pub fn identify_temporal_pattern(expr: &TLExpr) -> TemporalPattern {
108    match expr {
109        TLExpr::Always(e) if !is_temporal(e) => TemporalPattern::AlwaysP,
110
111        TLExpr::Eventually(e) if !is_temporal(e) => TemporalPattern::EventuallyP,
112
113        TLExpr::Eventually(e) if matches!(**e, TLExpr::Always(_)) => {
114            TemporalPattern::EventuallyAlwaysP
115        }
116
117        TLExpr::Always(e) if matches!(**e, TLExpr::Eventually(_)) => {
118            TemporalPattern::AlwaysEventuallyP
119        }
120
121        TLExpr::Until { .. } => TemporalPattern::PUntilQ,
122
123        TLExpr::Always(e) => {
124            if let TLExpr::Imply(_, then_branch) = &**e {
125                if matches!(**then_branch, TLExpr::Eventually(_)) {
126                    return TemporalPattern::AlwaysImpliesEventually;
127                } else if matches!(**then_branch, TLExpr::Next(_)) {
128                    return TemporalPattern::AlwaysImpliesNext;
129                }
130            }
131            TemporalPattern::Complex
132        }
133
134        _ => TemporalPattern::Complex,
135    }
136}
137
138/// Check if an expression contains temporal operators.
139pub fn is_temporal(expr: &TLExpr) -> bool {
140    match expr {
141        TLExpr::Next(_)
142        | TLExpr::Eventually(_)
143        | TLExpr::Always(_)
144        | TLExpr::Until { .. }
145        | TLExpr::Release { .. }
146        | TLExpr::WeakUntil { .. }
147        | TLExpr::StrongRelease { .. } => true,
148
149        TLExpr::And(l, r) | TLExpr::Or(l, r) | TLExpr::Imply(l, r) => {
150            is_temporal(l) || is_temporal(r)
151        }
152
153        TLExpr::Not(e) | TLExpr::Score(e) => is_temporal(e),
154
155        TLExpr::Exists { body, .. }
156        | TLExpr::ForAll { body, .. }
157        | TLExpr::SoftExists { body, .. }
158        | TLExpr::SoftForAll { body, .. } => is_temporal(body),
159
160        TLExpr::IfThenElse {
161            condition,
162            then_branch,
163            else_branch,
164        } => is_temporal(condition) || is_temporal(then_branch) || is_temporal(else_branch),
165
166        _ => false,
167    }
168}
169
170/// Compute temporal complexity metrics for a formula.
171pub fn compute_temporal_complexity(expr: &TLExpr) -> TemporalComplexity {
172    fn compute_rec(expr: &TLExpr, depth: usize) -> TemporalComplexity {
173        let mut metrics = TemporalComplexity {
174            temporal_depth: depth,
175            ..Default::default()
176        };
177
178        match expr {
179            TLExpr::Next(e) => {
180                metrics.next_count = 1;
181                metrics.temporal_op_count = 1;
182                let inner = compute_rec(e, depth + 1);
183                metrics.merge(inner);
184            }
185
186            TLExpr::Eventually(e) => {
187                metrics.temporal_op_count = 1;
188                let inner = compute_rec(e, depth + 1);
189                metrics.merge(inner);
190            }
191
192            TLExpr::Always(e) => {
193                metrics.temporal_op_count = 1;
194                // Check for fairness pattern: Always (P -> Eventually Q)
195                if let TLExpr::Imply(_, then_br) = &**e {
196                    if matches!(**then_br, TLExpr::Eventually(_)) {
197                        metrics.has_fairness = true;
198                    }
199                }
200                let inner = compute_rec(e, depth + 1);
201                metrics.merge(inner);
202            }
203
204            TLExpr::Until { before, after } => {
205                metrics.until_count = 1;
206                metrics.temporal_op_count = 1;
207                let before_metrics = compute_rec(before, depth + 1);
208                let after_metrics = compute_rec(after, depth + 1);
209                metrics.merge(before_metrics);
210                metrics.merge(after_metrics);
211            }
212
213            TLExpr::Release { released, releaser } => {
214                metrics.release_count = 1;
215                metrics.temporal_op_count = 1;
216                let released_metrics = compute_rec(released, depth + 1);
217                let releaser_metrics = compute_rec(releaser, depth + 1);
218                metrics.merge(released_metrics);
219                metrics.merge(releaser_metrics);
220            }
221
222            TLExpr::WeakUntil { before, after } => {
223                metrics.until_count = 1;
224                metrics.temporal_op_count = 1;
225                let before_metrics = compute_rec(before, depth);
226                let after_metrics = compute_rec(after, depth);
227                metrics.merge(before_metrics);
228                metrics.merge(after_metrics);
229            }
230
231            TLExpr::StrongRelease { released, releaser } => {
232                metrics.release_count = 1;
233                metrics.temporal_op_count = 1;
234                let released_metrics = compute_rec(released, depth);
235                let releaser_metrics = compute_rec(releaser, depth);
236                metrics.merge(released_metrics);
237                metrics.merge(releaser_metrics);
238            }
239
240            TLExpr::And(l, r) | TLExpr::Or(l, r) | TLExpr::Imply(l, r) => {
241                let left_metrics = compute_rec(l, depth);
242                let right_metrics = compute_rec(r, depth);
243                metrics.merge(left_metrics);
244                metrics.merge(right_metrics);
245            }
246
247            TLExpr::Not(e) | TLExpr::Score(e) => {
248                let inner = compute_rec(e, depth);
249                metrics.merge(inner);
250            }
251
252            _ => {}
253        }
254
255        metrics
256    }
257
258    compute_rec(expr, 0)
259}
260
261impl TemporalComplexity {
262    fn merge(&mut self, other: TemporalComplexity) {
263        self.temporal_depth = self.temporal_depth.max(other.temporal_depth);
264        self.temporal_op_count += other.temporal_op_count;
265        self.until_count += other.until_count;
266        self.release_count += other.release_count;
267        self.next_count += other.next_count;
268        self.has_fairness = self.has_fairness || other.has_fairness;
269    }
270}
271
272/// Extract all temporal subformulas from an expression.
273pub fn extract_temporal_subformulas(expr: &TLExpr) -> Vec<TLExpr> {
274    let mut result = Vec::new();
275    extract_temporal_rec(expr, &mut result);
276    result
277}
278
279fn extract_temporal_rec(expr: &TLExpr, result: &mut Vec<TLExpr>) {
280    match expr {
281        TLExpr::Next(_)
282        | TLExpr::Eventually(_)
283        | TLExpr::Always(_)
284        | TLExpr::Until { .. }
285        | TLExpr::Release { .. }
286        | TLExpr::WeakUntil { .. }
287        | TLExpr::StrongRelease { .. } => {
288            result.push(expr.clone());
289        }
290        _ => {}
291    }
292
293    // Recurse into children
294    match expr {
295        TLExpr::And(l, r) | TLExpr::Or(l, r) | TLExpr::Imply(l, r) => {
296            extract_temporal_rec(l, result);
297            extract_temporal_rec(r, result);
298        }
299        TLExpr::Not(e)
300        | TLExpr::Score(e)
301        | TLExpr::Next(e)
302        | TLExpr::Eventually(e)
303        | TLExpr::Always(e) => {
304            extract_temporal_rec(e, result);
305        }
306        TLExpr::Until { before, after }
307        | TLExpr::Release {
308            released: before,
309            releaser: after,
310        }
311        | TLExpr::WeakUntil { before, after }
312        | TLExpr::StrongRelease {
313            released: before,
314            releaser: after,
315        } => {
316            extract_temporal_rec(before, result);
317            extract_temporal_rec(after, result);
318        }
319        _ => {}
320    }
321}
322
323/// Convert temporal formula to safety-progress decomposition.
324///
325/// Every LTL formula can be expressed as the conjunction of a safety
326/// property and a liveness property.
327pub fn decompose_safety_liveness(expr: &TLExpr) -> (Option<TLExpr>, Option<TLExpr>) {
328    match expr {
329        // Always P is pure safety
330        TLExpr::Always(e) if !has_liveness(e) => (Some(expr.clone()), None),
331
332        // Eventually P is pure liveness
333        TLExpr::Eventually(e) if !has_safety(e) => (None, Some(expr.clone())),
334
335        // Conjunction: decompose both sides
336        TLExpr::And(left, right) => {
337            let (left_safety, left_liveness) = decompose_safety_liveness(left);
338            let (right_safety, right_liveness) = decompose_safety_liveness(right);
339
340            let safety = match (left_safety, right_safety) {
341                (Some(l), Some(r)) => Some(TLExpr::and(l, r)),
342                (Some(l), None) => Some(l),
343                (None, Some(r)) => Some(r),
344                (None, None) => None,
345            };
346
347            let liveness = match (left_liveness, right_liveness) {
348                (Some(l), Some(r)) => Some(TLExpr::and(l, r)),
349                (Some(l), None) => Some(l),
350                (None, Some(r)) => Some(r),
351                (None, None) => None,
352            };
353
354            (safety, liveness)
355        }
356
357        // Default: treat as mixed
358        _ => (Some(expr.clone()), None),
359    }
360}
361
362/// Check if formula contains liveness properties.
363fn has_liveness(expr: &TLExpr) -> bool {
364    match expr {
365        TLExpr::Eventually(_) => true,
366        TLExpr::Always(e) if matches!(**e, TLExpr::Eventually(_)) => true,
367        TLExpr::Until { .. } => true,
368        TLExpr::And(l, r) | TLExpr::Or(l, r) => has_liveness(l) || has_liveness(r),
369        TLExpr::Not(e) => has_liveness(e),
370        _ => false,
371    }
372}
373
374/// Check if formula contains safety properties.
375fn has_safety(expr: &TLExpr) -> bool {
376    match expr {
377        TLExpr::Always(e) if !matches!(**e, TLExpr::Eventually(_)) => true,
378        TLExpr::And(l, r) | TLExpr::Or(l, r) => has_safety(l) || has_safety(r),
379        TLExpr::Not(e) => has_safety(e),
380        _ => false,
381    }
382}
383
384/// Check if formula is in negation normal form for temporal operators.
385///
386/// In temporal NNF, negations only appear directly before predicates.
387pub fn is_temporal_nnf(expr: &TLExpr) -> bool {
388    match expr {
389        TLExpr::Not(e) => {
390            // Negation should only be in front of predicates or constants
391            matches!(**e, TLExpr::Pred { .. } | TLExpr::Constant(_))
392        }
393
394        TLExpr::And(l, r) | TLExpr::Or(l, r) | TLExpr::Imply(l, r) => {
395            is_temporal_nnf(l) && is_temporal_nnf(r)
396        }
397
398        TLExpr::Next(e) | TLExpr::Eventually(e) | TLExpr::Always(e) => is_temporal_nnf(e),
399
400        TLExpr::Until { before, after }
401        | TLExpr::Release {
402            released: before,
403            releaser: after,
404        }
405        | TLExpr::WeakUntil { before, after }
406        | TLExpr::StrongRelease {
407            released: before,
408            releaser: after,
409        } => is_temporal_nnf(before) && is_temporal_nnf(after),
410
411        _ => true,
412    }
413}
414
415/// Apply additional LTL equivalences beyond basic optimizations.
416///
417/// These are more advanced equivalences useful for model checking:
418/// - Absorption laws: GFP ∧ FGP ≡ FGP
419/// - Distributive laws: F(P ∨ Q) ≡ FP ∨ FQ
420/// - Expansion laws: FP ≡ P ∨ XFP
421pub fn apply_advanced_ltl_equivalences(expr: &TLExpr) -> TLExpr {
422    match expr {
423        // F(P ∨ Q) → FP ∨ FQ (distributive)
424        TLExpr::Eventually(e) => {
425            if let TLExpr::Or(left, right) = &**e {
426                return TLExpr::or(
427                    TLExpr::eventually(apply_advanced_ltl_equivalences(left)),
428                    TLExpr::eventually(apply_advanced_ltl_equivalences(right)),
429                );
430            }
431            TLExpr::eventually(apply_advanced_ltl_equivalences(e))
432        }
433
434        // G(P ∧ Q) → GP ∧ GQ (distributive)
435        TLExpr::Always(e) => {
436            if let TLExpr::And(left, right) = &**e {
437                return TLExpr::and(
438                    TLExpr::always(apply_advanced_ltl_equivalences(left)),
439                    TLExpr::always(apply_advanced_ltl_equivalences(right)),
440                );
441            }
442            TLExpr::always(apply_advanced_ltl_equivalences(e))
443        }
444
445        // Absorption: GFP ∧ FGP → FGP
446        TLExpr::And(left, right) => {
447            // Check for GFP ∧ FGP pattern
448            let is_gfp_left =
449                matches!(left.as_ref(), TLExpr::Always(e) if matches!(**e, TLExpr::Eventually(_)));
450            let is_fgp_right =
451                matches!(right.as_ref(), TLExpr::Eventually(e) if matches!(**e, TLExpr::Always(_)));
452
453            if is_gfp_left && is_fgp_right {
454                return apply_advanced_ltl_equivalences(right);
455            }
456
457            // Check reverse
458            let is_fgp_left =
459                matches!(left.as_ref(), TLExpr::Eventually(e) if matches!(**e, TLExpr::Always(_)));
460            let is_gfp_right =
461                matches!(right.as_ref(), TLExpr::Always(e) if matches!(**e, TLExpr::Eventually(_)));
462
463            if is_fgp_left && is_gfp_right {
464                return apply_advanced_ltl_equivalences(left);
465            }
466
467            TLExpr::and(
468                apply_advanced_ltl_equivalences(left),
469                apply_advanced_ltl_equivalences(right),
470            )
471        }
472
473        // Recurse for other operators
474        TLExpr::Or(l, r) => TLExpr::or(
475            apply_advanced_ltl_equivalences(l),
476            apply_advanced_ltl_equivalences(r),
477        ),
478
479        TLExpr::Imply(l, r) => TLExpr::imply(
480            apply_advanced_ltl_equivalences(l),
481            apply_advanced_ltl_equivalences(r),
482        ),
483
484        TLExpr::Not(e) => TLExpr::negate(apply_advanced_ltl_equivalences(e)),
485
486        TLExpr::Next(e) => TLExpr::next(apply_advanced_ltl_equivalences(e)),
487
488        TLExpr::Until { before, after } => TLExpr::until(
489            apply_advanced_ltl_equivalences(before),
490            apply_advanced_ltl_equivalences(after),
491        ),
492
493        _ => expr.clone(),
494    }
495}
496
497/// Extract state predicates (non-temporal atomic propositions).
498///
499/// Useful for model checking to identify the atomic propositions
500/// that need to be evaluated in each state.
501pub fn extract_state_predicates(expr: &TLExpr) -> HashSet<String> {
502    let mut predicates = HashSet::new();
503    extract_state_predicates_rec(expr, &mut predicates);
504    predicates
505}
506
507fn extract_state_predicates_rec(expr: &TLExpr, predicates: &mut HashSet<String>) {
508    match expr {
509        TLExpr::Pred { name, .. } => {
510            predicates.insert(name.clone());
511        }
512
513        TLExpr::And(l, r) | TLExpr::Or(l, r) | TLExpr::Imply(l, r) => {
514            extract_state_predicates_rec(l, predicates);
515            extract_state_predicates_rec(r, predicates);
516        }
517
518        TLExpr::Not(e)
519        | TLExpr::Score(e)
520        | TLExpr::Next(e)
521        | TLExpr::Eventually(e)
522        | TLExpr::Always(e) => {
523            extract_state_predicates_rec(e, predicates);
524        }
525
526        TLExpr::Until { before, after }
527        | TLExpr::Release {
528            released: before,
529            releaser: after,
530        } => {
531            extract_state_predicates_rec(before, predicates);
532            extract_state_predicates_rec(after, predicates);
533        }
534
535        _ => {}
536    }
537}
538
539#[cfg(test)]
540mod tests {
541    use super::*;
542    use crate::Term;
543
544    #[test]
545    fn test_classify_safety_property() {
546        let expr = TLExpr::always(TLExpr::pred("safe", vec![Term::var("x")]));
547        assert_eq!(classify_temporal_formula(&expr), TemporalClass::Safety);
548    }
549
550    #[test]
551    fn test_classify_liveness_property() {
552        let expr = TLExpr::eventually(TLExpr::pred("goal", vec![Term::var("x")]));
553        assert_eq!(classify_temporal_formula(&expr), TemporalClass::Liveness);
554    }
555
556    #[test]
557    fn test_classify_persistence() {
558        let expr = TLExpr::eventually(TLExpr::always(TLExpr::pred("stable", vec![])));
559        assert_eq!(classify_temporal_formula(&expr), TemporalClass::Persistence);
560    }
561
562    #[test]
563    fn test_classify_fairness() {
564        let request = TLExpr::pred("request", vec![]);
565        let grant = TLExpr::pred("grant", vec![]);
566        let expr = TLExpr::always(TLExpr::imply(request, TLExpr::eventually(grant)));
567        assert_eq!(classify_temporal_formula(&expr), TemporalClass::Fairness);
568    }
569
570    #[test]
571    fn test_identify_pattern_always() {
572        let expr = TLExpr::always(TLExpr::pred("P", vec![]));
573        assert_eq!(identify_temporal_pattern(&expr), TemporalPattern::AlwaysP);
574    }
575
576    #[test]
577    fn test_identify_pattern_eventually_always() {
578        let expr = TLExpr::eventually(TLExpr::always(TLExpr::pred("P", vec![])));
579        assert_eq!(
580            identify_temporal_pattern(&expr),
581            TemporalPattern::EventuallyAlwaysP
582        );
583    }
584
585    #[test]
586    fn test_is_temporal() {
587        let temporal = TLExpr::next(TLExpr::pred("P", vec![]));
588        assert!(is_temporal(&temporal));
589
590        let non_temporal = TLExpr::pred("P", vec![]);
591        assert!(!is_temporal(&non_temporal));
592    }
593
594    #[test]
595    fn test_temporal_complexity() {
596        let expr = TLExpr::until(
597            TLExpr::pred("P", vec![]),
598            TLExpr::eventually(TLExpr::pred("Q", vec![])),
599        );
600
601        let metrics = compute_temporal_complexity(&expr);
602        assert_eq!(metrics.until_count, 1);
603        assert_eq!(metrics.temporal_op_count, 2);
604        assert!(metrics.temporal_depth >= 1);
605    }
606
607    #[test]
608    fn test_extract_temporal_subformulas() {
609        let expr = TLExpr::and(
610            TLExpr::always(TLExpr::pred("P", vec![])),
611            TLExpr::eventually(TLExpr::pred("Q", vec![])),
612        );
613
614        let subformulas = extract_temporal_subformulas(&expr);
615        assert_eq!(subformulas.len(), 2);
616    }
617
618    #[test]
619    fn test_decompose_pure_safety() {
620        let expr = TLExpr::always(TLExpr::pred("safe", vec![]));
621        let (safety, liveness) = decompose_safety_liveness(&expr);
622        assert!(safety.is_some());
623        assert!(liveness.is_none());
624    }
625
626    #[test]
627    fn test_decompose_pure_liveness() {
628        let expr = TLExpr::eventually(TLExpr::pred("goal", vec![]));
629        let (safety, liveness) = decompose_safety_liveness(&expr);
630        assert!(safety.is_none());
631        assert!(liveness.is_some());
632    }
633
634    #[test]
635    fn test_is_temporal_nnf() {
636        // Positive case: negation only in front of predicate
637        let nnf = TLExpr::and(
638            TLExpr::pred("P", vec![]),
639            TLExpr::negate(TLExpr::pred("Q", vec![])),
640        );
641        assert!(is_temporal_nnf(&nnf));
642
643        // Negative case: negation in front of And
644        let not_nnf = TLExpr::negate(TLExpr::and(
645            TLExpr::pred("P", vec![]),
646            TLExpr::pred("Q", vec![]),
647        ));
648        assert!(!is_temporal_nnf(&not_nnf));
649    }
650
651    #[test]
652    fn test_distributive_eventually() {
653        let expr = TLExpr::eventually(TLExpr::or(
654            TLExpr::pred("P", vec![]),
655            TLExpr::pred("Q", vec![]),
656        ));
657
658        let result = apply_advanced_ltl_equivalences(&expr);
659        // Should distribute: F(P ∨ Q) → FP ∨ FQ
660        assert!(matches!(result, TLExpr::Or(_, _)));
661    }
662
663    #[test]
664    fn test_distributive_always() {
665        let expr = TLExpr::always(TLExpr::and(
666            TLExpr::pred("P", vec![]),
667            TLExpr::pred("Q", vec![]),
668        ));
669
670        let result = apply_advanced_ltl_equivalences(&expr);
671        // Should distribute: G(P ∧ Q) → GP ∧ GQ
672        assert!(matches!(result, TLExpr::And(_, _)));
673    }
674
675    #[test]
676    fn test_extract_state_predicates() {
677        let expr = TLExpr::and(
678            TLExpr::eventually(TLExpr::pred("P", vec![])),
679            TLExpr::always(TLExpr::pred("Q", vec![])),
680        );
681
682        let predicates = extract_state_predicates(&expr);
683        assert_eq!(predicates.len(), 2);
684        assert!(predicates.contains("P"));
685        assert!(predicates.contains("Q"));
686    }
687
688    #[test]
689    fn test_fairness_detection() {
690        let request = TLExpr::pred("req", vec![]);
691        let grant = TLExpr::pred("grant", vec![]);
692        let fairness = TLExpr::always(TLExpr::imply(request, TLExpr::eventually(grant)));
693
694        let metrics = compute_temporal_complexity(&fairness);
695        assert!(metrics.has_fairness);
696    }
697}