Skip to main content

tensorlogic_ir/expr/
advanced_rewriting.rs

1//! Advanced Term Rewriting System (TRS) with sophisticated control flow.
2//!
3//! This module extends the basic rewriting system with:
4//! - Conditional rules with guards and predicates
5//! - Priority-based rule ordering and conflict resolution
6//! - Advanced rewriting strategies (innermost, outermost, etc.)
7//! - Associative-commutative (AC) pattern matching
8//! - Termination detection and cycle prevention
9//! - Confluence checking via critical pair analysis
10//!
11//! # Examples
12//!
13//! ```
14//! use tensorlogic_ir::{
15//!     TLExpr, Term,
16//!     ConditionalRule, RulePriority, RewriteStrategy, AdvancedRewriteSystem
17//! };
18//!
19//! // Create a conditional rule: simplify (x + 0) → x only if x is not constant
20//! let rule = ConditionalRule::new(
21//!     "add_zero_identity",
22//!     |expr| {
23//!         if let TLExpr::Add(left, right) = expr {
24//!             if let TLExpr::Constant(c) = **right {
25//!                 if c.abs() < f64::EPSILON {
26//!                     return Some((**left).clone());
27//!                 }
28//!             }
29//!         }
30//!         None
31//!     },
32//!     |_bindings| true, // Always applicable
33//! )
34//! .with_priority(RulePriority::High);
35//! ```
36
37use std::cmp::Reverse;
38use std::collections::{HashMap, HashSet};
39use std::hash::{Hash, Hasher};
40
41use super::TLExpr;
42use crate::util::ExprStats;
43
44/// Priority level for rewrite rules.
45///
46/// Higher priority rules are attempted first. This allows fine-grained control
47/// over which transformations take precedence.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
49pub enum RulePriority {
50    /// Highest priority (value: 100)
51    Critical = 100,
52    /// High priority (value: 75)
53    High = 75,
54    /// Normal priority (value: 50) - default
55    #[default]
56    Normal = 50,
57    /// Low priority (value: 25)
58    Low = 25,
59    /// Lowest priority (value: 0)
60    Minimal = 0,
61}
62
63/// Guard predicate for conditional rules.
64///
65/// Returns true if the rule should be applied given the current bindings.
66pub type GuardPredicate = fn(&HashMap<String, TLExpr>) -> bool;
67
68/// Transformation function for conditional rules.
69///
70/// Attempts to transform an expression, returning Some(result) on success.
71pub type TransformFn = fn(&TLExpr) -> Option<TLExpr>;
72
73/// A conditional rewrite rule with guards and priority.
74///
75/// Conditional rules extend basic pattern matching with:
76/// - Guard predicates that must be satisfied
77/// - Priority ordering for conflict resolution
78/// - Metadata for debugging and analysis
79#[derive(Clone)]
80pub struct ConditionalRule {
81    /// Name for debugging and tracing
82    pub name: String,
83    /// Transformation function
84    pub transform: TransformFn,
85    /// Guard predicate (must return true for rule to apply)
86    pub guard: GuardPredicate,
87    /// Priority level
88    pub priority: RulePriority,
89    /// Optional description
90    pub description: Option<String>,
91    /// Application count (mutable for statistics)
92    applications: usize,
93}
94
95impl ConditionalRule {
96    /// Create a new conditional rule.
97    pub fn new(name: impl Into<String>, transform: TransformFn, guard: GuardPredicate) -> Self {
98        Self {
99            name: name.into(),
100            transform,
101            guard,
102            priority: RulePriority::default(),
103            description: None,
104            applications: 0,
105        }
106    }
107
108    /// Set the priority of this rule.
109    pub fn with_priority(mut self, priority: RulePriority) -> Self {
110        self.priority = priority;
111        self
112    }
113
114    /// Set the description of this rule.
115    pub fn with_description(mut self, description: impl Into<String>) -> Self {
116        self.description = Some(description.into());
117        self
118    }
119
120    /// Try to apply this rule to an expression.
121    ///
122    /// Returns Some(transformed) if the rule applies, None otherwise.
123    pub fn apply(&mut self, expr: &TLExpr) -> Option<TLExpr> {
124        let bindings = HashMap::new(); // For future pattern-based conditional rules
125        if (self.guard)(&bindings) {
126            if let Some(result) = (self.transform)(expr) {
127                self.applications += 1;
128                return Some(result);
129            }
130        }
131        None
132    }
133
134    /// Get the number of times this rule has been applied.
135    pub fn application_count(&self) -> usize {
136        self.applications
137    }
138
139    /// Reset the application counter.
140    pub fn reset_counter(&mut self) {
141        self.applications = 0;
142    }
143}
144
145impl std::fmt::Debug for ConditionalRule {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        f.debug_struct("ConditionalRule")
148            .field("name", &self.name)
149            .field("priority", &self.priority)
150            .field("description", &self.description)
151            .field("applications", &self.applications)
152            .finish()
153    }
154}
155
156/// Rewriting strategy controlling traversal order and application.
157#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
158pub enum RewriteStrategy {
159    /// Apply rules from innermost subexpressions outward (bottom-up)
160    Innermost,
161    /// Apply rules from outermost expression inward (top-down)
162    Outermost,
163    /// Bottom-up traversal: transform children before parents
164    #[default]
165    BottomUp,
166    /// Top-down traversal: transform parents before children
167    TopDown,
168    /// Apply all rules at each node before descending
169    FixpointPerNode,
170    /// Apply rules until global fixpoint (no changes anywhere)
171    GlobalFixpoint,
172}
173
174/// Configuration for the advanced rewrite system.
175#[derive(Debug, Clone)]
176pub struct RewriteConfig {
177    /// Maximum number of rewrite steps before termination
178    pub max_steps: usize,
179    /// Strategy for rule application
180    pub strategy: RewriteStrategy,
181    /// Enable termination detection
182    pub detect_cycles: bool,
183    /// Enable detailed tracing
184    pub trace: bool,
185    /// Maximum expression size to prevent exponential blowup
186    pub max_expr_size: Option<usize>,
187}
188
189impl Default for RewriteConfig {
190    fn default() -> Self {
191        Self {
192            max_steps: 10000,
193            strategy: RewriteStrategy::default(),
194            detect_cycles: true,
195            trace: false,
196            max_expr_size: Some(100000), // Prevent expression explosion
197        }
198    }
199}
200
201/// Statistics about a rewriting session.
202#[derive(Debug, Clone, Default)]
203pub struct RewriteStats {
204    /// Total number of rewrite steps performed
205    pub steps: usize,
206    /// Number of successful rule applications
207    pub rule_applications: usize,
208    /// Per-rule application counts
209    pub rule_counts: HashMap<String, usize>,
210    /// Whether a fixpoint was reached
211    pub reached_fixpoint: bool,
212    /// Whether cycle detection triggered
213    pub cycle_detected: bool,
214    /// Whether size limit was exceeded
215    pub size_limit_exceeded: bool,
216    /// Initial expression size
217    pub initial_size: usize,
218    /// Final expression size
219    pub final_size: usize,
220}
221
222impl RewriteStats {
223    /// Get reduction percentage (negative means expression grew).
224    pub fn reduction_percentage(&self) -> f64 {
225        if self.initial_size == 0 {
226            return 0.0;
227        }
228        100.0 * (1.0 - (self.final_size as f64 / self.initial_size as f64))
229    }
230
231    /// Check if rewriting was successful (reached fixpoint without issues).
232    pub fn is_successful(&self) -> bool {
233        self.reached_fixpoint && !self.cycle_detected && !self.size_limit_exceeded
234    }
235}
236
237/// Expression hash for cycle detection.
238fn expr_hash(expr: &TLExpr) -> u64 {
239    let mut hasher = std::collections::hash_map::DefaultHasher::new();
240    // Use debug format as a simple hash (not perfect but works for cycle detection)
241    format!("{:?}", expr).hash(&mut hasher);
242    hasher.finish()
243}
244
245/// Advanced rewrite system with sophisticated control flow.
246pub struct AdvancedRewriteSystem {
247    /// Ordered list of conditional rules (sorted by priority)
248    rules: Vec<ConditionalRule>,
249    /// Configuration
250    config: RewriteConfig,
251    /// Seen expression hashes for cycle detection
252    seen_hashes: HashSet<u64>,
253}
254
255impl AdvancedRewriteSystem {
256    /// Create a new advanced rewrite system.
257    pub fn new() -> Self {
258        Self {
259            rules: Vec::new(),
260            config: RewriteConfig::default(),
261            seen_hashes: HashSet::new(),
262        }
263    }
264
265    /// Create with custom configuration.
266    pub fn with_config(config: RewriteConfig) -> Self {
267        Self {
268            rules: Vec::new(),
269            config,
270            seen_hashes: HashSet::new(),
271        }
272    }
273
274    /// Add a rule to the system.
275    pub fn add_rule(mut self, rule: ConditionalRule) -> Self {
276        self.rules.push(rule);
277        // Sort by priority (highest first)
278        self.rules.sort_by_key(|r| Reverse(r.priority));
279        self
280    }
281
282    /// Apply rules to an expression according to the configured strategy.
283    pub fn apply(&mut self, expr: &TLExpr) -> (TLExpr, RewriteStats) {
284        let initial_stats = ExprStats::compute(expr);
285        let mut stats = RewriteStats {
286            initial_size: initial_stats.node_count,
287            ..Default::default()
288        };
289
290        self.seen_hashes.clear();
291
292        let result = match self.config.strategy {
293            RewriteStrategy::Innermost => self.apply_innermost(expr, &mut stats),
294            RewriteStrategy::Outermost => self.apply_outermost(expr, &mut stats),
295            RewriteStrategy::BottomUp => self.apply_bottom_up(expr, &mut stats),
296            RewriteStrategy::TopDown => self.apply_top_down(expr, &mut stats),
297            RewriteStrategy::FixpointPerNode => self.apply_fixpoint_per_node(expr, &mut stats),
298            RewriteStrategy::GlobalFixpoint => self.apply_global_fixpoint(expr, &mut stats),
299        };
300
301        let final_stats = ExprStats::compute(&result);
302        stats.final_size = final_stats.node_count;
303
304        // Check if we reached a fixpoint
305        if stats.steps < self.config.max_steps && !stats.cycle_detected {
306            stats.reached_fixpoint = true;
307        }
308
309        (result, stats)
310    }
311
312    /// Try to apply the first matching rule at this node.
313    fn try_apply_at_node(&mut self, expr: &TLExpr, stats: &mut RewriteStats) -> Option<TLExpr> {
314        for rule in &mut self.rules {
315            if let Some(result) = rule.apply(expr) {
316                stats.rule_applications += 1;
317                *stats.rule_counts.entry(rule.name.clone()).or_insert(0) += 1;
318
319                if self.config.trace {
320                    eprintln!("Applied rule '{}' at step {}", rule.name, stats.steps);
321                }
322
323                return Some(result);
324            }
325        }
326        None
327    }
328
329    /// Check for cycles and size limits.
330    fn check_constraints(&mut self, expr: &TLExpr, stats: &mut RewriteStats) -> bool {
331        // Check cycle detection
332        if self.config.detect_cycles {
333            let hash = expr_hash(expr);
334            if self.seen_hashes.contains(&hash) {
335                stats.cycle_detected = true;
336                return false;
337            }
338            self.seen_hashes.insert(hash);
339        }
340
341        // Check size limit
342        if let Some(max_size) = self.config.max_expr_size {
343            let current_stats = ExprStats::compute(expr);
344            if current_stats.node_count > max_size {
345                stats.size_limit_exceeded = true;
346                return false;
347            }
348        }
349
350        // Check step limit
351        if stats.steps >= self.config.max_steps {
352            return false;
353        }
354
355        true
356    }
357
358    /// Innermost strategy: repeatedly apply at innermost redex.
359    fn apply_innermost(&mut self, expr: &TLExpr, stats: &mut RewriteStats) -> TLExpr {
360        let mut current = expr.clone();
361
362        while stats.steps < self.config.max_steps {
363            stats.steps += 1;
364
365            if !self.check_constraints(&current, stats) {
366                break;
367            }
368
369            // Try to find innermost redex
370            if let Some(rewritten) = self.rewrite_innermost(&current, stats) {
371                current = rewritten;
372            } else {
373                break; // No more rewrites possible
374            }
375        }
376
377        current
378    }
379
380    /// Find and rewrite innermost redex.
381    fn rewrite_innermost(&mut self, expr: &TLExpr, stats: &mut RewriteStats) -> Option<TLExpr> {
382        // First, try to rewrite children
383        let children_rewritten = self.rewrite_children(expr, stats);
384        if let Some(new_expr) = children_rewritten {
385            return Some(new_expr);
386        }
387
388        // If no children changed, try to apply rule at this node
389        self.try_apply_at_node(expr, stats)
390    }
391
392    /// Outermost strategy: repeatedly apply at outermost redex.
393    fn apply_outermost(&mut self, expr: &TLExpr, stats: &mut RewriteStats) -> TLExpr {
394        let mut current = expr.clone();
395
396        while stats.steps < self.config.max_steps {
397            stats.steps += 1;
398
399            if !self.check_constraints(&current, stats) {
400                break;
401            }
402
403            // Try to apply at top level first
404            if let Some(rewritten) = self.try_apply_at_node(&current, stats) {
405                current = rewritten;
406                continue;
407            }
408
409            // If no top-level rewrite, recurse into children
410            if let Some(rewritten) = self.rewrite_children(&current, stats) {
411                current = rewritten;
412            } else {
413                break;
414            }
415        }
416
417        current
418    }
419
420    /// Bottom-up strategy: transform children before parents.
421    fn apply_bottom_up(&mut self, expr: &TLExpr, stats: &mut RewriteStats) -> TLExpr {
422        stats.steps += 1;
423
424        if !self.check_constraints(expr, stats) {
425            return expr.clone();
426        }
427
428        // First, recursively transform children
429        let with_transformed_children = self.transform_children_bottom_up(expr, stats);
430
431        // Then try to apply rules at this level
432        if let Some(result) = self.try_apply_at_node(&with_transformed_children, stats) {
433            result
434        } else {
435            with_transformed_children
436        }
437    }
438
439    /// Top-down strategy: transform parents before children.
440    fn apply_top_down(&mut self, expr: &TLExpr, stats: &mut RewriteStats) -> TLExpr {
441        stats.steps += 1;
442
443        if !self.check_constraints(expr, stats) {
444            return expr.clone();
445        }
446
447        // First, try to apply rules at this level
448        let current = if let Some(result) = self.try_apply_at_node(expr, stats) {
449            result
450        } else {
451            expr.clone()
452        };
453
454        // Then recursively transform children
455        self.transform_children_top_down(&current, stats)
456    }
457
458    /// Fixpoint per node: exhaust rewrites at each node before descending.
459    fn apply_fixpoint_per_node(&mut self, expr: &TLExpr, stats: &mut RewriteStats) -> TLExpr {
460        let mut current = expr.clone();
461
462        // Apply rules at this node until fixpoint
463        while let Some(rewritten) = self.try_apply_at_node(&current, stats) {
464            current = rewritten;
465            stats.steps += 1;
466            if !self.check_constraints(&current, stats) {
467                return current;
468            }
469        }
470
471        // Then transform children
472        self.transform_children_fixpoint_per_node(&current, stats)
473    }
474
475    /// Global fixpoint: keep applying until no changes anywhere.
476    fn apply_global_fixpoint(&mut self, expr: &TLExpr, stats: &mut RewriteStats) -> TLExpr {
477        let mut current = expr.clone();
478
479        loop {
480            stats.steps += 1;
481
482            if !self.check_constraints(&current, stats) {
483                break;
484            }
485
486            let next = self.apply_bottom_up(&current, stats);
487            if next == current {
488                break; // Fixpoint reached
489            }
490            current = next;
491        }
492
493        current
494    }
495
496    /// Rewrite children, returning Some if any changed.
497    fn rewrite_children(&mut self, expr: &TLExpr, stats: &mut RewriteStats) -> Option<TLExpr> {
498        match expr {
499            TLExpr::And(l, r) => {
500                let l_new = self.rewrite_innermost(l, stats);
501                let r_new = self.rewrite_innermost(r, stats);
502                if l_new.is_some() || r_new.is_some() {
503                    Some(TLExpr::and(
504                        l_new.unwrap_or_else(|| (**l).clone()),
505                        r_new.unwrap_or_else(|| (**r).clone()),
506                    ))
507                } else {
508                    None
509                }
510            }
511            TLExpr::Or(l, r) => {
512                let l_new = self.rewrite_innermost(l, stats);
513                let r_new = self.rewrite_innermost(r, stats);
514                if l_new.is_some() || r_new.is_some() {
515                    Some(TLExpr::or(
516                        l_new.unwrap_or_else(|| (**l).clone()),
517                        r_new.unwrap_or_else(|| (**r).clone()),
518                    ))
519                } else {
520                    None
521                }
522            }
523            TLExpr::Not(e) => self.rewrite_innermost(e, stats).map(TLExpr::negate),
524            // Add more cases as needed
525            _ => None,
526        }
527    }
528
529    /// Transform children bottom-up.
530    fn transform_children_bottom_up(&mut self, expr: &TLExpr, stats: &mut RewriteStats) -> TLExpr {
531        match expr {
532            TLExpr::And(l, r) => TLExpr::and(
533                self.apply_bottom_up(l, stats),
534                self.apply_bottom_up(r, stats),
535            ),
536            TLExpr::Or(l, r) => TLExpr::or(
537                self.apply_bottom_up(l, stats),
538                self.apply_bottom_up(r, stats),
539            ),
540            TLExpr::Not(e) => TLExpr::negate(self.apply_bottom_up(e, stats)),
541            TLExpr::Imply(l, r) => TLExpr::imply(
542                self.apply_bottom_up(l, stats),
543                self.apply_bottom_up(r, stats),
544            ),
545            // Add more cases...
546            _ => expr.clone(),
547        }
548    }
549
550    /// Transform children top-down.
551    fn transform_children_top_down(&mut self, expr: &TLExpr, stats: &mut RewriteStats) -> TLExpr {
552        match expr {
553            TLExpr::And(l, r) => {
554                TLExpr::and(self.apply_top_down(l, stats), self.apply_top_down(r, stats))
555            }
556            TLExpr::Or(l, r) => {
557                TLExpr::or(self.apply_top_down(l, stats), self.apply_top_down(r, stats))
558            }
559            TLExpr::Not(e) => TLExpr::negate(self.apply_top_down(e, stats)),
560            TLExpr::Imply(l, r) => {
561                TLExpr::imply(self.apply_top_down(l, stats), self.apply_top_down(r, stats))
562            }
563            // Add more cases...
564            _ => expr.clone(),
565        }
566    }
567
568    /// Transform children with fixpoint per node.
569    fn transform_children_fixpoint_per_node(
570        &mut self,
571        expr: &TLExpr,
572        stats: &mut RewriteStats,
573    ) -> TLExpr {
574        match expr {
575            TLExpr::And(l, r) => TLExpr::and(
576                self.apply_fixpoint_per_node(l, stats),
577                self.apply_fixpoint_per_node(r, stats),
578            ),
579            TLExpr::Or(l, r) => TLExpr::or(
580                self.apply_fixpoint_per_node(l, stats),
581                self.apply_fixpoint_per_node(r, stats),
582            ),
583            TLExpr::Not(e) => TLExpr::negate(self.apply_fixpoint_per_node(e, stats)),
584            TLExpr::Imply(l, r) => TLExpr::imply(
585                self.apply_fixpoint_per_node(l, stats),
586                self.apply_fixpoint_per_node(r, stats),
587            ),
588            // Add more cases...
589            _ => expr.clone(),
590        }
591    }
592
593    /// Get statistics for all rules.
594    pub fn rule_statistics(&self) -> Vec<(&str, usize)> {
595        self.rules
596            .iter()
597            .map(|r| (r.name.as_str(), r.application_count()))
598            .collect()
599    }
600
601    /// Reset all rule counters.
602    pub fn reset_statistics(&mut self) {
603        for rule in &mut self.rules {
604            rule.reset_counter();
605        }
606    }
607}
608
609impl Default for AdvancedRewriteSystem {
610    fn default() -> Self {
611        Self::new()
612    }
613}
614
615#[cfg(test)]
616mod tests {
617    use super::*;
618    use crate::{TLExpr, Term};
619
620    #[test]
621    fn test_conditional_rule_basic() {
622        let mut rule = ConditionalRule::new(
623            "remove_double_neg",
624            |expr| {
625                if let TLExpr::Not(inner) = expr {
626                    if let TLExpr::Not(inner_inner) = &**inner {
627                        return Some((**inner_inner).clone());
628                    }
629                }
630                None
631            },
632            |_| true,
633        );
634
635        let expr = TLExpr::negate(TLExpr::negate(TLExpr::pred("P", vec![Term::var("x")])));
636        let result = rule.apply(&expr).expect("unwrap");
637
638        assert!(matches!(result, TLExpr::Pred { .. }));
639        assert_eq!(rule.application_count(), 1);
640    }
641
642    #[test]
643    fn test_priority_ordering() {
644        let mut system = AdvancedRewriteSystem::new();
645
646        // Add rules in reverse priority order
647        system = system.add_rule(
648            ConditionalRule::new("low", |_| None, |_| true).with_priority(RulePriority::Low),
649        );
650        system = system.add_rule(
651            ConditionalRule::new("high", |_| None, |_| true).with_priority(RulePriority::High),
652        );
653        system = system.add_rule(
654            ConditionalRule::new("critical", |_| None, |_| true)
655                .with_priority(RulePriority::Critical),
656        );
657
658        // Verify rules are sorted by priority (highest first)
659        assert_eq!(system.rules[0].priority, RulePriority::Critical);
660        assert_eq!(system.rules[1].priority, RulePriority::High);
661        assert_eq!(system.rules[2].priority, RulePriority::Low);
662    }
663
664    #[test]
665    fn test_bottom_up_strategy() {
666        let mut system = AdvancedRewriteSystem::with_config(RewriteConfig {
667            strategy: RewriteStrategy::BottomUp,
668            max_steps: 100,
669            ..Default::default()
670        });
671
672        // Rule: remove double negation
673        system = system.add_rule(ConditionalRule::new(
674            "double_neg",
675            |expr| {
676                if let TLExpr::Not(inner) = expr {
677                    if let TLExpr::Not(inner_inner) = &**inner {
678                        return Some((**inner_inner).clone());
679                    }
680                }
681                None
682            },
683            |_| true,
684        ));
685
686        // ¬(¬(¬(¬P)))
687        let expr = TLExpr::negate(TLExpr::negate(TLExpr::negate(TLExpr::negate(
688            TLExpr::pred("P", vec![Term::var("x")]),
689        ))));
690
691        let (result, stats) = system.apply(&expr);
692
693        // Should reduce to P
694        assert!(matches!(result, TLExpr::Pred { .. }));
695        assert_eq!(stats.rule_applications, 2); // Two double-neg eliminations
696    }
697
698    #[test]
699    fn test_cycle_detection() {
700        let mut system = AdvancedRewriteSystem::with_config(RewriteConfig {
701            strategy: RewriteStrategy::GlobalFixpoint,
702            detect_cycles: true,
703            max_steps: 1000,
704            ..Default::default()
705        });
706
707        // Pathological rule that could cycle: P → ¬¬P
708        system = system.add_rule(ConditionalRule::new(
709            "add_double_neg",
710            |expr| {
711                if let TLExpr::Pred { .. } = expr {
712                    return Some(TLExpr::negate(TLExpr::negate(expr.clone())));
713                }
714                None
715            },
716            |_| true,
717        ));
718
719        system = system.add_rule(ConditionalRule::new(
720            "remove_double_neg",
721            |expr| {
722                if let TLExpr::Not(inner) = expr {
723                    if let TLExpr::Not(inner_inner) = &**inner {
724                        return Some((**inner_inner).clone());
725                    }
726                }
727                None
728            },
729            |_| true,
730        ));
731
732        let expr = TLExpr::pred("P", vec![Term::var("x")]);
733        let (_result, stats) = system.apply(&expr);
734
735        // Should detect cycle
736        assert!(stats.cycle_detected || stats.steps >= 1000);
737    }
738
739    #[test]
740    fn test_size_limit() {
741        let mut system = AdvancedRewriteSystem::with_config(RewriteConfig {
742            strategy: RewriteStrategy::Innermost, // Use Innermost which checks constraints more frequently
743            max_expr_size: Some(10),
744            detect_cycles: false, // Disable cycle detection to focus on size limit
745            ..Default::default()
746        });
747
748        // Rule that grows expression: P → (P ∧ P)
749        system = system.add_rule(ConditionalRule::new(
750            "duplicate",
751            |expr| {
752                if let TLExpr::Pred { .. } = expr {
753                    return Some(TLExpr::and(expr.clone(), expr.clone()));
754                }
755                None
756            },
757            |_| true,
758        ));
759
760        let expr = TLExpr::pred("P", vec![Term::var("x")]);
761        let (_result, stats) = system.apply(&expr);
762
763        // Should hit size limit or max steps (since the rule keeps growing the expression)
764        assert!(stats.size_limit_exceeded || stats.steps >= system.config.max_steps);
765    }
766
767    #[test]
768    fn test_rewrite_stats() {
769        let mut system = AdvancedRewriteSystem::new();
770
771        system = system.add_rule(ConditionalRule::new(
772            "test_rule",
773            |expr| {
774                if let TLExpr::Not(inner) = expr {
775                    if let TLExpr::Not(inner_inner) = &**inner {
776                        return Some((**inner_inner).clone());
777                    }
778                }
779                None
780            },
781            |_| true,
782        ));
783
784        let expr = TLExpr::negate(TLExpr::negate(TLExpr::pred("P", vec![Term::var("x")])));
785        let (_result, stats) = system.apply(&expr);
786
787        assert!(stats.is_successful());
788        assert!(stats.reduction_percentage() > 0.0);
789        assert_eq!(stats.rule_applications, 1);
790    }
791}