Skip to main content

oxiz_solver/propagation/
backtrack_manager.rs

1//! Backtrack Manager for SMT Solver.
2//!
3//! Manages:
4//! - Decision stack and backtracking
5//! - Trail of assignments
6//! - Undo/redo operations
7//! - Incremental solving state
8
9#[allow(unused_imports)]
10use crate::prelude::*;
11use oxiz_core::ast::TermId;
12
13/// Backtrack manager for incremental SMT solving.
14pub struct BacktrackManager {
15    /// Decision stack: levels and their decision literals
16    decision_stack: Vec<DecisionLevel>,
17    /// Current decision level
18    current_level: usize,
19    /// Trail of all assignments
20    trail: Vec<Assignment>,
21    /// Variable to trail index mapping
22    var_to_trail: FxHashMap<TermId, usize>,
23    /// Undo stack for theory-specific operations
24    undo_stack: Vec<UndoAction>,
25    /// Checkpoint stack for incremental solving
26    checkpoints: Vec<Checkpoint>,
27    /// Statistics
28    stats: BacktrackStats,
29}
30
31/// A decision level in the search.
32#[derive(Debug, Clone)]
33pub struct DecisionLevel {
34    /// Level number
35    pub level: usize,
36    /// Decision literal at this level (if any)
37    pub decision: Option<Assignment>,
38    /// Trail position at start of this level
39    pub trail_start: usize,
40}
41
42/// An assignment to a variable.
43#[derive(Debug, Clone)]
44pub struct Assignment {
45    /// Variable being assigned
46    pub var: TermId,
47    /// Assigned value
48    pub value: bool,
49    /// Decision level at which this was assigned
50    pub level: usize,
51    /// Reason for assignment (None = decision, Some = propagation)
52    pub reason: Option<ReasonClause>,
53}
54
55/// Reason for a propagation.
56#[derive(Debug, Clone)]
57pub enum ReasonClause {
58    /// Boolean clause that caused propagation
59    BooleanClause(Vec<TermId>),
60    /// Theory propagation
61    TheoryPropagation(TheoryReason),
62}
63
64/// Theory-specific reason for propagation.
65#[derive(Debug, Clone)]
66pub struct TheoryReason {
67    /// Theory identifier
68    pub theory_id: usize,
69    /// Theory-specific explanation
70    pub explanation: Vec<TermId>,
71}
72
73/// Undo action for theory operations.
74#[derive(Debug, Clone)]
75pub enum UndoAction {
76    /// Undo an equality assertion
77    UndoEquality(TermId, TermId),
78    /// Undo a bound update
79    UndoBound(TermId, BoundUpdate),
80    /// Undo an array store
81    UndoArrayStore(TermId),
82    /// Generic theory undo with callback identifier
83    TheoryUndo(usize, Vec<TermId>),
84}
85
86/// Bound update information.
87#[derive(Debug, Clone)]
88pub struct BoundUpdate {
89    /// Old lower bound
90    pub old_lower: Option<i64>,
91    /// Old upper bound
92    pub old_upper: Option<i64>,
93}
94
95/// Checkpoint for incremental solving.
96#[derive(Debug, Clone)]
97pub struct Checkpoint {
98    /// Decision level at checkpoint
99    pub level: usize,
100    /// Trail size at checkpoint
101    pub trail_size: usize,
102    /// Undo stack size at checkpoint
103    pub undo_size: usize,
104}
105
106/// Backtrack statistics.
107#[derive(Debug, Clone, Default)]
108pub struct BacktrackStats {
109    /// Number of decisions made
110    pub decisions: usize,
111    /// Number of propagations
112    pub propagations: usize,
113    /// Number of backtracks
114    pub backtracks: usize,
115    /// Number of full restarts
116    pub restarts: usize,
117    /// Number of undo operations
118    pub undos: usize,
119    /// Maximum decision level reached
120    pub max_level: usize,
121}
122
123impl BacktrackManager {
124    /// Create a new backtrack manager.
125    pub fn new() -> Self {
126        Self {
127            decision_stack: vec![DecisionLevel {
128                level: 0,
129                decision: None,
130                trail_start: 0,
131            }],
132            current_level: 0,
133            trail: Vec::new(),
134            var_to_trail: FxHashMap::default(),
135            undo_stack: Vec::new(),
136            checkpoints: Vec::new(),
137            stats: BacktrackStats::default(),
138        }
139    }
140
141    /// Make a decision: assign variable at a new decision level.
142    pub fn decide(&mut self, var: TermId, value: bool) -> Result<(), String> {
143        self.current_level += 1;
144        self.stats.decisions += 1;
145
146        if self.current_level > self.stats.max_level {
147            self.stats.max_level = self.current_level;
148        }
149
150        let assignment = Assignment {
151            var,
152            value,
153            level: self.current_level,
154            reason: None, // Decision has no reason
155        };
156
157        // Record trail position
158        let trail_start = self.trail.len();
159
160        // Push decision level
161        self.decision_stack.push(DecisionLevel {
162            level: self.current_level,
163            decision: Some(assignment.clone()),
164            trail_start,
165        });
166
167        // Add to trail
168        self.assign(assignment)?;
169
170        Ok(())
171    }
172
173    /// Propagate: assign variable due to constraint propagation.
174    pub fn propagate(
175        &mut self,
176        var: TermId,
177        value: bool,
178        reason: ReasonClause,
179    ) -> Result<(), String> {
180        self.stats.propagations += 1;
181
182        let assignment = Assignment {
183            var,
184            value,
185            level: self.current_level,
186            reason: Some(reason),
187        };
188
189        self.assign(assignment)?;
190
191        Ok(())
192    }
193
194    /// Assign a variable.
195    fn assign(&mut self, assignment: Assignment) -> Result<(), String> {
196        // Check if already assigned
197        if let Some(&trail_idx) = self.var_to_trail.get(&assignment.var) {
198            let existing = &self.trail[trail_idx];
199            if existing.value != assignment.value {
200                return Err(format!(
201                    "Conflict: variable {:?} already assigned differently",
202                    assignment.var
203                ));
204            }
205            // Already assigned with same value
206            return Ok(());
207        }
208
209        // Record trail position
210        let trail_idx = self.trail.len();
211        self.var_to_trail.insert(assignment.var, trail_idx);
212
213        // Add to trail
214        self.trail.push(assignment);
215
216        Ok(())
217    }
218
219    /// Backtrack to a specific decision level.
220    pub fn backtrack(&mut self, target_level: usize) -> Result<(), String> {
221        if target_level > self.current_level {
222            return Err("Cannot backtrack to higher level".to_string());
223        }
224
225        if target_level == self.current_level {
226            return Ok(()); // Nothing to do
227        }
228
229        self.stats.backtracks += 1;
230
231        // Find trail position for target level
232        // We want to keep assignments at target_level, so we use the trail_start of the next level
233        let target_trail_pos = if target_level + 1 < self.decision_stack.len() {
234            self.decision_stack[target_level + 1].trail_start
235        } else {
236            // No assignments to remove (shouldn't happen if backtracking to lower level)
237            self.trail.len()
238        };
239
240        // Undo assignments
241        while self.trail.len() > target_trail_pos {
242            if let Some(assignment) = self.trail.pop() {
243                self.var_to_trail.remove(&assignment.var);
244            }
245        }
246
247        // Undo theory operations
248        self.undo_to_level(target_level)?;
249
250        // Update decision stack
251        self.decision_stack.truncate(target_level + 1);
252        self.current_level = target_level;
253
254        Ok(())
255    }
256
257    /// Restart: backtrack to level 0.
258    pub fn restart(&mut self) -> Result<(), String> {
259        self.stats.restarts += 1;
260        self.backtrack(0)
261    }
262
263    /// Record an undo action.
264    pub fn record_undo(&mut self, action: UndoAction) {
265        self.undo_stack.push(action);
266    }
267
268    /// Undo theory operations to target level.
269    fn undo_to_level(&mut self, _target_level: usize) -> Result<(), String> {
270        // Find how many undo operations to perform
271        // (Simplified: undo all operations beyond target level)
272
273        let mut undos_to_apply = Vec::new();
274
275        // Collect undo operations (in reverse order)
276        // Pop directly instead of checking last() then popping
277        while let Some(undo) = self.undo_stack.pop() {
278            undos_to_apply.push(undo);
279
280            // Stop condition (simplified)
281            if undos_to_apply.len() > 100 {
282                break;
283            }
284        }
285
286        // Apply undo operations
287        for undo in undos_to_apply {
288            self.apply_undo(undo)?;
289            self.stats.undos += 1;
290        }
291
292        Ok(())
293    }
294
295    /// Apply an undo operation.
296    fn apply_undo(&mut self, _action: UndoAction) -> Result<(), String> {
297        // Theory-specific undo logic would go here
298        // This is a placeholder
299        Ok(())
300    }
301
302    /// Create a checkpoint for incremental solving.
303    pub fn push_checkpoint(&mut self) {
304        self.checkpoints.push(Checkpoint {
305            level: self.current_level,
306            trail_size: self.trail.len(),
307            undo_size: self.undo_stack.len(),
308        });
309    }
310
311    /// Pop to most recent checkpoint.
312    pub fn pop_checkpoint(&mut self) -> Result<(), String> {
313        if let Some(checkpoint) = self.checkpoints.pop() {
314            self.backtrack(checkpoint.level)?;
315
316            // Restore undo stack size
317            self.undo_stack.truncate(checkpoint.undo_size);
318
319            Ok(())
320        } else {
321            Err("No checkpoint to pop".to_string())
322        }
323    }
324
325    /// Get current decision level.
326    pub fn current_level(&self) -> usize {
327        self.current_level
328    }
329
330    /// Check if variable is assigned.
331    pub fn is_assigned(&self, var: TermId) -> bool {
332        self.var_to_trail.contains_key(&var)
333    }
334
335    /// Get assignment for variable.
336    pub fn get_assignment(&self, var: TermId) -> Option<&Assignment> {
337        if let Some(&trail_idx) = self.var_to_trail.get(&var) {
338            self.trail.get(trail_idx)
339        } else {
340            None
341        }
342    }
343
344    /// Get all assignments at current level.
345    pub fn current_assignments(&self) -> &[Assignment] {
346        &self.trail
347    }
348
349    /// Get decision at specific level.
350    pub fn get_decision(&self, level: usize) -> Option<&Assignment> {
351        if level < self.decision_stack.len() {
352            self.decision_stack[level].decision.as_ref()
353        } else {
354            None
355        }
356    }
357
358    /// Get statistics.
359    pub fn stats(&self) -> &BacktrackStats {
360        &self.stats
361    }
362
363    /// Reset statistics.
364    pub fn reset_stats(&mut self) {
365        self.stats = BacktrackStats::default();
366    }
367}
368
369impl Default for BacktrackManager {
370    fn default() -> Self {
371        Self::new()
372    }
373}
374
375#[cfg(test)]
376mod tests {
377    use super::*;
378
379    #[test]
380    fn test_backtrack_manager() {
381        let mgr = BacktrackManager::new();
382        assert_eq!(mgr.current_level(), 0);
383        assert_eq!(mgr.stats.decisions, 0);
384    }
385
386    #[test]
387    fn test_decide() {
388        let mut mgr = BacktrackManager::new();
389
390        let var = TermId::from(1);
391        mgr.decide(var, true)
392            .expect("test operation should succeed");
393
394        assert_eq!(mgr.current_level(), 1);
395        assert_eq!(mgr.stats.decisions, 1);
396        assert!(mgr.is_assigned(var));
397    }
398
399    #[test]
400    fn test_propagate() {
401        let mut mgr = BacktrackManager::new();
402
403        let var1 = TermId::from(1);
404        mgr.decide(var1, true)
405            .expect("test operation should succeed");
406
407        let var2 = TermId::from(2);
408        let reason = ReasonClause::BooleanClause(vec![var1]);
409        mgr.propagate(var2, false, reason)
410            .expect("test operation should succeed");
411
412        assert_eq!(mgr.stats.propagations, 1);
413        assert!(mgr.is_assigned(var2));
414    }
415
416    #[test]
417    fn test_backtrack() {
418        let mut mgr = BacktrackManager::new();
419
420        let var1 = TermId::from(1);
421        mgr.decide(var1, true)
422            .expect("test operation should succeed");
423
424        let var2 = TermId::from(2);
425        mgr.decide(var2, false)
426            .expect("test operation should succeed");
427
428        assert_eq!(mgr.current_level(), 2);
429
430        mgr.backtrack(1).expect("test operation should succeed");
431
432        assert_eq!(mgr.current_level(), 1);
433        assert!(mgr.is_assigned(var1));
434        assert!(!mgr.is_assigned(var2));
435    }
436
437    #[test]
438    fn test_restart() {
439        let mut mgr = BacktrackManager::new();
440
441        mgr.decide(TermId::from(1), true)
442            .expect("test operation should succeed");
443        mgr.decide(TermId::from(2), false)
444            .expect("test operation should succeed");
445
446        mgr.restart().expect("test operation should succeed");
447
448        assert_eq!(mgr.current_level(), 0);
449        assert_eq!(mgr.stats.restarts, 1);
450        assert_eq!(mgr.trail.len(), 0);
451    }
452
453    #[test]
454    fn test_checkpoint() {
455        let mut mgr = BacktrackManager::new();
456
457        mgr.decide(TermId::from(1), true)
458            .expect("test operation should succeed");
459        mgr.push_checkpoint();
460
461        mgr.decide(TermId::from(2), false)
462            .expect("test operation should succeed");
463
464        mgr.pop_checkpoint().expect("test operation should succeed");
465
466        assert_eq!(mgr.current_level(), 1);
467        assert!(!mgr.is_assigned(TermId::from(2)));
468    }
469
470    #[test]
471    fn test_get_assignment() {
472        let mut mgr = BacktrackManager::new();
473
474        let var = TermId::from(1);
475        mgr.decide(var, true)
476            .expect("test operation should succeed");
477
478        let assignment = mgr
479            .get_assignment(var)
480            .expect("test operation should succeed");
481        assert!(assignment.value);
482        assert_eq!(assignment.level, 1);
483        assert!(assignment.reason.is_none());
484    }
485}