Skip to main content

windjammer_runtime/
contracts.rs

1//! Design-by-contract utilities
2//!
3//! Provides runtime support for preconditions, postconditions, and invariants.
4
5use std::fmt;
6
7/// Contract violation error
8#[derive(Debug, Clone)]
9pub struct ContractViolation {
10    pub kind: ContractKind,
11    pub condition: String,
12    pub message: Option<String>,
13}
14
15impl ContractViolation {
16    pub fn new(kind: ContractKind, condition: String) -> Self {
17        Self {
18            kind,
19            condition,
20            message: None,
21        }
22    }
23
24    pub fn with_message(mut self, message: String) -> Self {
25        self.message = Some(message);
26        self
27    }
28}
29
30impl fmt::Display for ContractViolation {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        write!(f, "{} violation: {}", self.kind, self.condition)?;
33        if let Some(msg) = &self.message {
34            write!(f, " ({})", msg)?;
35        }
36        Ok(())
37    }
38}
39
40impl std::error::Error for ContractViolation {}
41
42/// Contract kind
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum ContractKind {
45    Precondition,
46    Postcondition,
47    Invariant,
48}
49
50impl fmt::Display for ContractKind {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        match self {
53            ContractKind::Precondition => write!(f, "Precondition"),
54            ContractKind::Postcondition => write!(f, "Postcondition"),
55            ContractKind::Invariant => write!(f, "Invariant"),
56        }
57    }
58}
59
60/// Check a precondition (requires)
61///
62/// # Example
63/// ```
64/// use windjammer_runtime::contracts::requires;
65///
66/// fn divide(a: i32, b: i32) -> i32 {
67///     requires(b != 0, "divisor must be non-zero");
68///     a / b
69/// }
70/// ```
71pub fn requires(condition: bool, description: &str) {
72    if !condition {
73        panic!(
74            "{}",
75            ContractViolation::new(ContractKind::Precondition, description.to_string())
76        );
77    }
78}
79
80/// Check a postcondition (ensures)
81///
82/// # Example
83/// ```
84/// use windjammer_runtime::contracts::ensures;
85///
86/// fn abs(x: i32) -> i32 {
87///     let result = if x < 0 { -x } else { x };
88///     ensures(result >= 0, "result must be non-negative");
89///     result
90/// }
91/// ```
92pub fn ensures(condition: bool, description: &str) {
93    if !condition {
94        panic!(
95            "{}",
96            ContractViolation::new(ContractKind::Postcondition, description.to_string())
97        );
98    }
99}
100
101/// Check an invariant
102///
103/// # Example
104/// ```
105/// use windjammer_runtime::contracts::invariant;
106///
107/// struct Counter {
108///     count: i32,
109/// }
110///
111/// impl Counter {
112///     fn increment(&mut self) {
113///         self.count += 1;
114///         invariant(self.count >= 0, "count must be non-negative");
115///     }
116/// }
117/// ```
118pub fn invariant(condition: bool, description: &str) {
119    if !condition {
120        panic!(
121            "{}",
122            ContractViolation::new(ContractKind::Invariant, description.to_string())
123        );
124    }
125}
126
127/// Helper for capturing "old" values in postconditions
128///
129/// # Example
130/// ```
131/// use windjammer_runtime::contracts::old;
132///
133/// fn increment(x: &mut i32) {
134///     let old_x = old(*x);
135///     *x += 1;
136///     assert_eq!(*x, old_x + 1);
137/// }
138/// ```
139#[inline(always)]
140pub fn old<T: Clone>(value: T) -> T {
141    value.clone()
142}
143
144/// Contract builder for complex contracts
145pub struct Contract {
146    preconditions: Vec<(bool, String)>,
147    postconditions: Vec<(bool, String)>,
148    invariants: Vec<(bool, String)>,
149}
150
151impl Contract {
152    pub fn new() -> Self {
153        Self {
154            preconditions: Vec::new(),
155            postconditions: Vec::new(),
156            invariants: Vec::new(),
157        }
158    }
159
160    pub fn requires(mut self, condition: bool, description: &str) -> Self {
161        self.preconditions
162            .push((condition, description.to_string()));
163        self
164    }
165
166    pub fn ensures(mut self, condition: bool, description: &str) -> Self {
167        self.postconditions
168            .push((condition, description.to_string()));
169        self
170    }
171
172    pub fn invariant(mut self, condition: bool, description: &str) -> Self {
173        self.invariants.push((condition, description.to_string()));
174        self
175    }
176
177    pub fn check_preconditions(&self) {
178        for (condition, desc) in &self.preconditions {
179            if !condition {
180                panic!(
181                    "{}",
182                    ContractViolation::new(ContractKind::Precondition, desc.clone())
183                );
184            }
185        }
186    }
187
188    pub fn check_postconditions(&self) {
189        for (condition, desc) in &self.postconditions {
190            if !condition {
191                panic!(
192                    "{}",
193                    ContractViolation::new(ContractKind::Postcondition, desc.clone())
194                );
195            }
196        }
197    }
198
199    pub fn check_invariants(&self) {
200        for (condition, desc) in &self.invariants {
201            if !condition {
202                panic!(
203                    "{}",
204                    ContractViolation::new(ContractKind::Invariant, desc.clone())
205                );
206            }
207        }
208    }
209}
210
211impl Default for Contract {
212    fn default() -> Self {
213        Self::new()
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    #[test]
222    fn test_requires_success() {
223        requires(true, "should not panic");
224        // Test passes
225    }
226
227    #[test]
228    #[should_panic(expected = "Precondition violation")]
229    fn test_requires_failure() {
230        requires(false, "should panic");
231    }
232
233    #[test]
234    fn test_ensures_success() {
235        ensures(true, "should not panic");
236        // Test passes
237    }
238
239    #[test]
240    #[should_panic(expected = "Postcondition violation")]
241    fn test_ensures_failure() {
242        ensures(false, "should panic");
243    }
244
245    #[test]
246    fn test_invariant_success() {
247        invariant(true, "should not panic");
248        // Test passes
249    }
250
251    #[test]
252    #[should_panic(expected = "Invariant violation")]
253    fn test_invariant_failure() {
254        invariant(false, "should panic");
255    }
256
257    #[test]
258    fn test_old() {
259        let x = 42;
260        let old_x = old(x);
261        assert_eq!(old_x, 42);
262    }
263
264    #[test]
265    fn test_contract_builder() {
266        let contract = Contract::new()
267            .requires(true, "x > 0")
268            .requires(true, "y > 0")
269            .ensures(true, "result > 0");
270
271        contract.check_preconditions();
272        contract.check_postconditions();
273    }
274
275    #[test]
276    #[should_panic(expected = "Precondition violation")]
277    fn test_contract_precondition_failure() {
278        let contract = Contract::new().requires(false, "x > 0");
279        contract.check_preconditions();
280    }
281
282    #[test]
283    #[should_panic(expected = "Postcondition violation")]
284    fn test_contract_postcondition_failure() {
285        let contract = Contract::new().ensures(false, "result > 0");
286        contract.check_postconditions();
287    }
288
289    #[test]
290    #[should_panic(expected = "Invariant violation")]
291    fn test_contract_invariant_failure() {
292        let contract = Contract::new().invariant(false, "count >= 0");
293        contract.check_invariants();
294    }
295
296    #[test]
297    fn test_contract_violation_display() {
298        let violation = ContractViolation::new(ContractKind::Precondition, "x > 0".to_string());
299        assert_eq!(violation.to_string(), "Precondition violation: x > 0");
300    }
301
302    #[test]
303    fn test_contract_violation_with_message() {
304        let violation = ContractViolation::new(ContractKind::Precondition, "x > 0".to_string())
305            .with_message("x must be positive".to_string());
306        assert_eq!(
307            violation.to_string(),
308            "Precondition violation: x > 0 (x must be positive)"
309        );
310    }
311}