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}