Skip to main content

kimberlite_properties/
lib.rs

1//! Antithesis-style property annotations for Deterministic Simulation Testing.
2//!
3//! This crate provides lightweight `macro_rules!` macros for annotating code with
4//! temporal property assertions inspired by the [Antithesis SDK](https://antithesis.com/docs/properties_assertions/assertions/).
5//!
6//! # Property Types
7//!
8//! - **ALWAYS**: Condition must hold every time it is evaluated. A single `false` is a violation.
9//! - **SOMETIMES**: Condition must be `true` at least once per simulation run. Guides exploration.
10//! - **NEVER**: Condition must never be `true`. Equivalent to `always!(!condition)`.
11//! - **REACHED**: Code path must be reached at least once per simulation run.
12//! - **UNREACHABLE**: Code path must never be reached. Violation on first reach.
13//!
14//! # Zero-Cost in Production
15//!
16//! Without `feature = "sim"`, all macros compile to nothing. No runtime overhead.
17//! With `feature = "sim"`, assertions record to thread-local registries for VOPR tracking.
18//!
19//! # Usage
20//!
21//! ```ignore
22//! use kimberlite_properties::{always, sometimes, never, reached, unreachable_property};
23//!
24//! fn apply_committed(state: State, cmd: Command) -> Result<(State, Vec<Effect>)> {
25//!     let old_offset = state.offset();
26//!     let (new_state, effects) = process(state, cmd)?;
27//!
28//!     always!(
29//!         new_state.offset() >= old_offset,
30//!         "offset_monotonicity",
31//!         "applied offset must never decrease"
32//!     );
33//!
34//!     sometimes!(
35//!         effects.len() > 3,
36//!         "batch_effects",
37//!         "simulation should sometimes produce large effect batches"
38//!     );
39//!
40//!     Ok((new_state, effects))
41//! }
42//! ```
43
44pub mod registry;
45
46// ============================================================================
47// Property Macros
48// ============================================================================
49
50/// Asserts that a condition is ALWAYS true when evaluated.
51///
52/// In simulation mode, records every evaluation. A single `false` evaluation
53/// is reported as a property violation.
54///
55/// In production builds, compiles to nothing.
56///
57/// # Arguments
58/// - `$cond` - Boolean condition to check
59/// - `$id` - String literal property identifier (must be unique across codebase)
60/// - `$msg` - Human-readable description of what this property means
61#[macro_export]
62macro_rules! always {
63    ($cond:expr, $id:literal, $msg:literal) => {
64        #[cfg(any(test, feature = "sim"))]
65        {
66            let condition = $cond;
67            $crate::registry::record_always($id, condition, $msg);
68            assert!(
69                condition,
70                concat!("ALWAYS property violated [", $id, "]: ", $msg)
71            );
72        }
73    };
74}
75
76/// Asserts that a condition is true at least once per simulation run.
77///
78/// This is a **coverage signal**, not a bug catcher. It tells the simulator
79/// "go find a state where this is true." The simulator preferentially explores
80/// paths toward triggering `sometimes` properties that haven't been satisfied yet.
81///
82/// In production builds, compiles to nothing.
83///
84/// # Arguments
85/// - `$cond` - Boolean condition to check
86/// - `$id` - String literal property identifier (must be unique across codebase)
87/// - `$msg` - Human-readable description of what this property means
88#[macro_export]
89macro_rules! sometimes {
90    ($cond:expr, $id:literal, $msg:literal) => {
91        #[cfg(any(test, feature = "sim"))]
92        {
93            $crate::registry::record_sometimes($id, $cond, $msg);
94        }
95    };
96}
97
98/// Asserts that a condition is NEVER true when evaluated.
99///
100/// Equivalent to `always!(!condition, ...)` but reads more naturally for
101/// expressing safety invariants (e.g., "two leaders NEVER exist in the same view").
102///
103/// In production builds, compiles to nothing.
104///
105/// # Arguments
106/// - `$cond` - Boolean condition that must NOT be true
107/// - `$id` - String literal property identifier (must be unique across codebase)
108/// - `$msg` - Human-readable description of what this property means
109#[macro_export]
110macro_rules! never {
111    ($cond:expr, $id:literal, $msg:literal) => {
112        #[cfg(any(test, feature = "sim"))]
113        {
114            let condition = $cond;
115            $crate::registry::record_never($id, condition, $msg);
116            assert!(
117                !condition,
118                concat!("NEVER property violated [", $id, "]: ", $msg)
119            );
120        }
121    };
122}
123
124/// Asserts that a code path is reached at least once per simulation run.
125///
126/// Place this at interesting code locations (error handlers, rare branches,
127/// fault recovery paths) to ensure simulation exercises them.
128///
129/// In production builds, compiles to nothing.
130///
131/// # Arguments
132/// - `$id` - String literal property identifier (must be unique across codebase)
133/// - `$msg` - Human-readable description of what this code path represents
134#[macro_export]
135macro_rules! reached {
136    ($id:literal, $msg:literal) => {
137        #[cfg(any(test, feature = "sim"))]
138        {
139            $crate::registry::record_reached($id, $msg);
140        }
141    };
142}
143
144/// Asserts that a code path is NEVER reached.
145///
146/// Place this at locations that should be dead code or impossible states.
147/// Reaching this location is a property violation.
148///
149/// In production builds, compiles to nothing.
150///
151/// # Arguments
152/// - `$id` - String literal property identifier (must be unique across codebase)
153/// - `$msg` - Human-readable description of why this path is unreachable
154#[macro_export]
155macro_rules! unreachable_property {
156    ($id:literal, $msg:literal) => {
157        #[cfg(any(test, feature = "sim"))]
158        {
159            $crate::registry::record_unreachable($id, $msg);
160            panic!(concat!("UNREACHABLE property violated [", $id, "]: ", $msg));
161        }
162    };
163}
164
165#[cfg(test)]
166mod tests {
167    // Macros should compile and work in test mode (which has cfg(test))
168
169    #[test]
170    fn test_always_passing() {
171        always!(true, "test.always_pass", "always true in test");
172    }
173
174    #[test]
175    #[should_panic(expected = "ALWAYS property violated")]
176    fn test_always_failing() {
177        always!(false, "test.always_fail", "should panic");
178    }
179
180    #[test]
181    fn test_sometimes_records() {
182        sometimes!(true, "test.sometimes_true", "recorded as satisfied");
183        sometimes!(false, "test.sometimes_false", "recorded as unsatisfied");
184    }
185
186    #[test]
187    fn test_never_passing() {
188        never!(false, "test.never_pass", "never true in test");
189    }
190
191    #[test]
192    #[should_panic(expected = "NEVER property violated")]
193    fn test_never_failing() {
194        never!(true, "test.never_fail", "should panic");
195    }
196
197    #[test]
198    fn test_reached() {
199        reached!("test.reached", "code path exercised");
200    }
201
202    #[test]
203    #[should_panic(expected = "UNREACHABLE property violated")]
204    fn test_unreachable() {
205        unreachable_property!("test.unreachable", "should panic");
206    }
207
208    #[test]
209    fn test_registry_snapshot() {
210        use crate::registry;
211
212        registry::reset();
213
214        always!(true, "snap.always", "test");
215        sometimes!(true, "snap.sometimes_hit", "test");
216        sometimes!(false, "snap.sometimes_miss", "test");
217        never!(false, "snap.never", "test");
218        reached!("snap.reached", "test");
219
220        let snapshot = registry::snapshot();
221
222        // ALWAYS property evaluated once, never violated
223        let always_prop = snapshot.get("snap.always").unwrap();
224        assert_eq!(always_prop.evaluations, 1);
225        assert_eq!(always_prop.violations, 0);
226
227        // SOMETIMES satisfied
228        let sometimes_hit = snapshot.get("snap.sometimes_hit").unwrap();
229        assert!(sometimes_hit.satisfied);
230
231        // SOMETIMES not satisfied
232        let sometimes_miss = snapshot.get("snap.sometimes_miss").unwrap();
233        assert!(!sometimes_miss.satisfied);
234
235        // NEVER property evaluated once, never violated
236        let never_prop = snapshot.get("snap.never").unwrap();
237        assert_eq!(never_prop.evaluations, 1);
238        assert_eq!(never_prop.violations, 0);
239
240        // REACHED
241        let reached_prop = snapshot.get("snap.reached").unwrap();
242        assert!(reached_prop.satisfied);
243    }
244}