Skip to main content

rexact_core/
limits.rs

1//! Limits manager (architecture section 6, RULE-08, FR-G05).
2//!
3//! Contract: one [`Limits`] value is passed explicitly to every computation;
4//! a [`Budget`] tracks consumption and records *which* limit stopped a run.
5//! Invariants: counters use checked arithmetic and saturate into a limit error,
6//! never wrap (NFR-C05). Failure mode: every breach maps to a catalogue code
7//! whose exit class is "unknown" (2) or a usage error (3).
8
9use rexact_syntax::error::ErrorCode;
10use std::time::Duration;
11
12/// Monotonic clock that is inert on WebAssembly (where `Instant` panics):
13/// there the time limit is not enforced and other limits still apply.
14#[derive(Debug, Clone, Copy)]
15pub struct Clock {
16    #[cfg(not(target_arch = "wasm32"))]
17    start: std::time::Instant,
18}
19
20impl Clock {
21    /// Start now.
22    pub fn now() -> Self {
23        Clock {
24            #[cfg(not(target_arch = "wasm32"))]
25            start: std::time::Instant::now(),
26        }
27    }
28    /// Time since start (always zero on WebAssembly).
29    pub fn elapsed(&self) -> Duration {
30        #[cfg(not(target_arch = "wasm32"))]
31        {
32            self.start.elapsed()
33        }
34        #[cfg(target_arch = "wasm32")]
35        {
36            Duration::ZERO
37        }
38    }
39}
40
41/// Configured limits. All fields are public so they can be reported verbatim
42/// in results.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct Limits {
45    /// Maximum automaton / product states explored (`RX-L001`).
46    pub states: u64,
47    /// Wall-clock budget (`RX-L002`).
48    pub time: Duration,
49    /// Estimated memory ceiling in bytes (`RX-L003`).
50    pub memory_bytes: u64,
51    /// Maximum counted-repetition expansion (`RX-L004`).
52    pub repeat: u64,
53    /// Maximum pattern length in Unicode scalar values (`RX-L005`).
54    pub length: u64,
55    /// Maximum nesting depth (`RX-L006`).
56    pub depth: u32,
57    /// Maximum enumerated items for witnesses / synthesis (`RX-L007`).
58    pub enumerate: u64,
59}
60
61impl Default for Limits {
62    fn default() -> Self {
63        Self {
64            states: 200_000,
65            time: Duration::from_millis(5_000),
66            memory_bytes: 1024 * 1024 * 1024,
67            repeat: 10_000,
68            length: 1_000_000,
69            depth: 250,
70            enumerate: 100_000,
71        }
72    }
73}
74
75/// Tracks consumption against [`Limits`] and remembers the first breach.
76#[derive(Debug)]
77pub struct Budget {
78    limits: Limits,
79    start: Clock,
80    states: u64,
81    memory: u64,
82    hit: Option<ErrorCode>,
83}
84
85impl Budget {
86    /// Start a budget; the clock starts now.
87    pub fn new(limits: Limits) -> Self {
88        Self {
89            limits,
90            start: Clock::now(),
91            states: 0,
92            memory: 0,
93            hit: None,
94        }
95    }
96
97    /// The configured limits (reported in results).
98    pub fn limits(&self) -> &Limits {
99        &self.limits
100    }
101
102    /// The first limit that stopped the run, if any (`--trace-limits`).
103    pub fn limit_hit(&self) -> Option<ErrorCode> {
104        self.hit
105    }
106
107    fn trip(&mut self, code: ErrorCode) -> Result<(), ErrorCode> {
108        // Keep the first breach: later ones are consequences of it.
109        self.hit.get_or_insert(code);
110        Err(code)
111    }
112
113    /// Account for `n` new states. Overflow counts as a breach.
114    pub fn add_states(&mut self, n: u64) -> Result<(), ErrorCode> {
115        match self.states.checked_add(n) {
116            Some(total) if total <= self.limits.states => {
117                self.states = total;
118                Ok(())
119            }
120            _ => self.trip(ErrorCode::L001),
121        }
122    }
123
124    /// Account for `bytes` of estimated memory.
125    pub fn add_memory(&mut self, bytes: u64) -> Result<(), ErrorCode> {
126        match self.memory.checked_add(bytes) {
127            Some(total) if total <= self.limits.memory_bytes => {
128                self.memory = total;
129                Ok(())
130            }
131            _ => self.trip(ErrorCode::L003),
132        }
133    }
134
135    /// Cooperative time check; call at loop boundaries.
136    pub fn check_time(&mut self) -> Result<(), ErrorCode> {
137        if self.start.elapsed() > self.limits.time {
138            self.trip(ErrorCode::L002)
139        } else {
140            Ok(())
141        }
142    }
143
144    /// Check a counted-repetition bound before expanding it.
145    pub fn check_repeat(&mut self, count: u64) -> Result<(), ErrorCode> {
146        if count > self.limits.repeat {
147            self.trip(ErrorCode::L004)
148        } else {
149            Ok(())
150        }
151    }
152
153    /// Check pattern length.
154    pub fn check_length(&mut self, len: u64) -> Result<(), ErrorCode> {
155        if len > self.limits.length {
156            self.trip(ErrorCode::L005)
157        } else {
158            Ok(())
159        }
160    }
161
162    /// Check nesting depth.
163    pub fn check_depth(&mut self, depth: u32) -> Result<(), ErrorCode> {
164        if depth > self.limits.depth {
165            self.trip(ErrorCode::L006)
166        } else {
167            Ok(())
168        }
169    }
170
171    /// Check an enumeration count.
172    pub fn check_enumerate(&mut self, count: u64) -> Result<(), ErrorCode> {
173        if count > self.limits.enumerate {
174            self.trip(ErrorCode::L007)
175        } else {
176            Ok(())
177        }
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    fn small() -> Limits {
186        Limits {
187            states: 10,
188            repeat: 5,
189            length: 8,
190            depth: 3,
191            enumerate: 4,
192            ..Limits::default()
193        }
194    }
195
196    #[test]
197    fn states_breach_reports_l001_and_remembers_it() {
198        let mut b = Budget::new(small());
199        assert!(b.add_states(10).is_ok());
200        assert_eq!(b.add_states(1), Err(ErrorCode::L001));
201        assert_eq!(b.limit_hit(), Some(ErrorCode::L001));
202    }
203
204    #[test]
205    fn first_breach_wins() {
206        let mut b = Budget::new(small());
207        let _ = b.check_depth(99);
208        let _ = b.check_repeat(99);
209        assert_eq!(b.limit_hit(), Some(ErrorCode::L006));
210    }
211
212    #[test]
213    fn overflow_is_a_breach_not_a_wrap() {
214        let mut b = Budget::new(Limits {
215            states: u64::MAX,
216            ..small()
217        });
218        assert!(b.add_states(u64::MAX).is_ok());
219        assert_eq!(b.add_states(1), Err(ErrorCode::L001));
220        let mut m = Budget::new(Limits {
221            memory_bytes: u64::MAX,
222            ..small()
223        });
224        assert!(m.add_memory(u64::MAX).is_ok());
225        assert_eq!(m.add_memory(1), Err(ErrorCode::L003));
226    }
227
228    #[test]
229    fn boundaries_are_inclusive() {
230        let mut b = Budget::new(small());
231        assert!(b.check_repeat(5).is_ok());
232        assert_eq!(b.check_repeat(6), Err(ErrorCode::L004));
233        let mut b = Budget::new(small());
234        assert!(b.check_length(8).is_ok());
235        assert_eq!(b.check_length(9), Err(ErrorCode::L005));
236        let mut b = Budget::new(small());
237        assert!(b.check_depth(3).is_ok());
238        assert_eq!(b.check_depth(4), Err(ErrorCode::L006));
239        let mut b = Budget::new(small());
240        assert!(b.check_enumerate(4).is_ok());
241        assert_eq!(b.check_enumerate(5), Err(ErrorCode::L007));
242    }
243
244    #[test]
245    fn time_limit_trips_l002() {
246        let mut b = Budget::new(Limits {
247            time: Duration::ZERO,
248            ..small()
249        });
250        std::thread::sleep(Duration::from_millis(2));
251        assert_eq!(b.check_time(), Err(ErrorCode::L002));
252    }
253
254    #[test]
255    fn memory_breach_reports_l003() {
256        let mut b = Budget::new(Limits {
257            memory_bytes: 100,
258            ..small()
259        });
260        assert!(b.add_memory(100).is_ok());
261        assert_eq!(b.add_memory(1), Err(ErrorCode::L003));
262    }
263}