pnc-engine 0.2.0

Core engine for PNC: arbitrary-precision RPN evaluation, units, and persistence
Documentation
use crate::value::Value;

pub const DEFAULT_HISTORY_DEPTH: usize = 10;
pub const DEFAULT_HISTORY_CAPACITY: usize = DEFAULT_HISTORY_DEPTH + 1;

#[derive(Debug, Clone)]
pub struct History {
    snapshots: Vec<Vec<Value>>,
    position: usize,
    capacity: usize,
}

impl Default for History {
    fn default() -> Self {
        Self::new()
    }
}

impl History {
    pub fn new() -> Self {
        Self::with_capacity(DEFAULT_HISTORY_CAPACITY)
    }

    pub fn with_capacity(capacity: usize) -> Self {
        Self {
            snapshots: Vec::new(),
            position: 0,
            capacity,
        }
    }

    pub fn capacity(&self) -> usize {
        self.capacity
    }

    pub fn is_enabled(&self) -> bool {
        self.capacity >= 2
    }

    pub fn position(&self) -> usize {
        self.position
    }

    pub fn len(&self) -> usize {
        self.snapshots.len()
    }

    pub fn is_empty(&self) -> bool {
        self.snapshots.is_empty()
    }

    pub fn can_undo(&self) -> bool {
        self.position > 0
    }

    pub fn can_redo(&self) -> bool {
        self.position + 1 < self.snapshots.len()
    }

    pub fn undo_count(&self) -> usize {
        self.position
    }

    pub fn redo_count(&self) -> usize {
        if self.snapshots.is_empty() {
            0
        } else {
            self.snapshots.len() - 1 - self.position
        }
    }

    pub fn undo(&mut self) -> Option<Vec<Value>> {
        if !self.is_enabled() || self.position == 0 {
            return None;
        }
        self.position -= 1;
        Some(self.snapshots[self.position].clone())
    }

    pub fn redo(&mut self) -> Option<Vec<Value>> {
        if !self.is_enabled() || self.position + 1 >= self.snapshots.len() {
            return None;
        }
        self.position += 1;
        Some(self.snapshots[self.position].clone())
    }

    pub fn record(&mut self, snapshot: Vec<Value>) {
        if !self.is_enabled() {
            return;
        }

        // Truncate forward/redo history.
        self.snapshots.truncate(self.position + 1);

        // Skip append if equal to last snapshot.
        if !self.snapshots.is_empty() && snapshot == *self.snapshots.last().unwrap() {
            return;
        }

        // Evict oldest if at capacity.
        if self.snapshots.len() == self.capacity && self.capacity > 0 {
            self.snapshots.remove(0);
            if self.position > 0 {
                self.position -= 1;
            }
        }

        self.snapshots.push(snapshot);
        self.position = self.snapshots.len() - 1;
    }

    pub fn seed(&mut self, snapshot: Vec<Value>) {
        if !self.is_enabled() {
            return;
        }
        self.snapshots = vec![snapshot];
        self.position = 0;
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::value::Value;

    #[test]
    fn undo_disabled_at_zero_capacity() {
        let mut h = History::with_capacity(0);
        assert!(!h.is_enabled());
        h.record(vec![Value::Int(1.into())]);
        assert_eq!(h.len(), 0);
        assert!(h.undo().is_none());
        assert!(h.redo().is_none());
    }

    #[test]
    fn capacity_one_disables() {
        let h = History::with_capacity(1);
        assert!(!h.is_enabled());
    }

    #[test]
    fn capacity_three_transition() {
        let mut h = History::with_capacity(3);
        h.seed(vec![Value::Int(0.into())]); // S0
        h.record(vec![Value::Int(1.into())]); // S1
        h.record(vec![Value::Int(2.into())]); // S2
        assert_eq!(h.len(), 3);
        assert_eq!(h.position(), 2);
        h.record(vec![Value::Int(3.into())]); // S3 -> evict S0
        assert_eq!(h.len(), 3);
        assert_eq!(h.position(), 2);
        // undo -> S2
        let v = h.undo().unwrap();
        assert_eq!(v, vec![Value::Int(2.into())]);
        assert_eq!(h.position(), 1);
        // redo -> S3
        let v = h.redo().unwrap();
        assert_eq!(v, vec![Value::Int(3.into())]);
        assert_eq!(h.position(), 2);
    }

    #[test]
    fn mutation_after_undo_drops_redo() {
        let mut h = History::with_capacity(5);
        h.seed(vec![Value::Int(0.into())]);
        h.record(vec![Value::Int(1.into())]);
        h.record(vec![Value::Int(2.into())]);
        h.undo(); // position 1
        h.record(vec![Value::Int(99.into())]); // truncate + append
        assert_eq!(h.redo_count(), 0);
    }

    #[test]
    fn no_op_result_truncates_redo_no_append() {
        let mut h = History::with_capacity(5);
        h.seed(vec![Value::Int(1.into())]);
        h.record(vec![Value::Int(2.into())]);
        h.undo(); // position 0
        // record same as last before undo... actually record S1 again
        // Simpler: record equal snapshot
        h.record(vec![Value::Int(1.into())]);
        // Should truncate redo (none here) and skip append since equal to S0
        // Actually after undo position=0, snapshots=[S0], record S0 -> equal -> skip
        assert_eq!(h.len(), 1);
    }

    #[test]
    fn redo_count_empty_buffer() {
        let h = History::with_capacity(5);
        assert_eq!(h.redo_count(), 0);
    }

    #[test]
    fn seed_empty_stack() {
        let mut h = History::with_capacity(5);
        h.seed(vec![]);
        assert_eq!(h.len(), 1);
        assert_eq!(h.position(), 0);
        let v = h.undo();
        assert!(v.is_none()); // position 0, can't undo
    }

    #[test]
    fn new_delegates() {
        let h = History::new();
        assert_eq!(h.capacity(), DEFAULT_HISTORY_CAPACITY);
    }
}