├── .gitignore ├── README.md ├── test_crate ├── .gitignore ├── src │ ├── lib.rs │ └── fast_assert_macro_tests.rs └── Cargo.toml ├── fast_assert ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md └── src │ └── lib.rs ├── Cargo.toml ├── Cargo.lock └── .github └── workflows └── rust.yml /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | fast_assert/README.md -------------------------------------------------------------------------------- /test_crate/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /fast_assert/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /test_crate/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod fast_assert_macro_tests; 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "fast_assert", 4 | "test_crate", 5 | ] 6 | -------------------------------------------------------------------------------- /fast_assert/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "fast_assert" 7 | version = "0.1.0" 8 | -------------------------------------------------------------------------------- /test_crate/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test_crate" 3 | version = "0.1.0" 4 | edition = "2024" 5 | publish = false 6 | authors = ["Sergey \"Shnatsel\" Davidoff"] 7 | license = "MIT OR Apache-2.0" 8 | 9 | [dependencies] 10 | fast_assert = {path = "../fast_assert"} 11 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "fast_assert" 7 | version = "0.1.2" 8 | 9 | [[package]] 10 | name = "test_crate" 11 | version = "0.1.0" 12 | dependencies = [ 13 | "fast_assert", 14 | ] 15 | -------------------------------------------------------------------------------- /fast_assert/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fast_assert" 3 | version = "0.1.2" 4 | edition = "2024" 5 | authors = ["Sergey \"Shnatsel\" Davidoff"] 6 | repository = "https://github.com/Shnatsel/fast_assert.git" 7 | license = "MIT OR Apache-2.0" 8 | keywords = ["assert", "assertion"] 9 | categories = ["rust-patterns"] 10 | description = "A faster assert!" 11 | 12 | [dependencies] 13 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Build 20 | run: cargo build --workspace --verbose 21 | - name: Run tests 22 | run: cargo test --workspace --verbose 23 | -------------------------------------------------------------------------------- /fast_assert/README.md: -------------------------------------------------------------------------------- 1 | # A faster `assert!` for Rust 2 | 3 | A drop-in replacement for the standard library's [`assert!`](https://doc.rust-lang.org/stable/std/macro.assert.html) 4 | macro that emits far less code in the hot function where the assertion holds. This reduces instruction cache pressure, 5 | and may allow for more optimizations by the compiler due to more aggressive inlining of hot functions. 6 | 7 | `fast_assert!` only adds [two](https://rust.godbolt.org/z/14hnj39sv) extra instructions to the hot path for the default error message. 8 | For a custom error message that prints more variables, it only adds 9 | [one jump instruction plus one instruction per printed variable](https://rust.godbolt.org/z/fo4refc1d). 10 | The standard library's `assert!` adds [five](https://rust.godbolt.org/z/Gczn8Ts54) instructions 11 | to the hot path for the default error message and [lots](https://rust.godbolt.org/z/hY5dGMPsh) for a custom error message. 12 | 13 | ## How? 14 | 15 | We defer all the work that needs to be done in case of a panic, such as formatting the arguments, 16 | to separate functions annotated with `#[cold]`. That way the function that calls `fast_assert!` 17 | can stay as lean as possible. 18 | 19 | By comparison, the std `assert!` emits some of the code executed only in case of a panic 20 | inside the function that invokes it. Even if that code isn't executed, you still pay a (small) price 21 | for it being present inside your hot function. 22 | 23 | ## Why not improve the standard library instead? 24 | 25 | The standard library's `assert!` is implemented not as a macro, but as a compiler built-in, 26 | which makes it difficult to modify and contribute to. 27 | 28 | We use a closure to defer all argument formatting to a separate cold function. 29 | This works identically to std `assert!` the vast majority of the time, 30 | but there might be some edge cases in which it would break, so such a change might not be acceptable 31 | for the standard library, at least outside a new language edition. Or maybe it's fine - who knows? 32 | 33 | Please take this as an invitation to improve the std `assert!` and make this crate obsolete! 34 | -------------------------------------------------------------------------------- /test_crate/src/fast_assert_macro_tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use fast_assert::fast_assert; 4 | use std::panic::{self, PanicHookInfo}; 5 | 6 | /// Verifies that the reported source code location for the panic is in THIS file, 7 | /// and not somewhere in fast_assert internals. 8 | /// Causes the process to abort with "panicked while panicking" message on failure. 9 | fn verify_source_code_location(f: F) { 10 | // Set a custom panic hook. 11 | let default_hook = panic::take_hook(); 12 | panic::set_hook(Box::new(|info: &PanicHookInfo| { 13 | // Extract the location from PanicInfo. 14 | if let Some(location) = info.location() { 15 | // Verify that the reported location is THIS file, 16 | // not the internals of fast_assert, which would be useless 17 | assert_eq!(location.file(), "test_crate/src/fast_assert_macro_tests.rs"); 18 | } 19 | })); 20 | 21 | // Run the user's code. 22 | f(); 23 | 24 | // Restore the default panic hook. 25 | panic::set_hook(default_hook); 26 | } 27 | 28 | #[test] 29 | fn holds() { 30 | fast_assert!(0 < 100); 31 | } 32 | 33 | #[test] 34 | #[should_panic] 35 | fn fails() { 36 | fast_assert!(100 < 0); 37 | } 38 | 39 | #[test] 40 | fn holds_custom_message() { 41 | let x = 0; 42 | let y = 100; 43 | fast_assert!(x < y, "x ({}) should be less than y ({})", x, y); 44 | } 45 | 46 | #[test] 47 | #[should_panic] 48 | fn fails_custom_message() { 49 | let x = 100; 50 | let y = 0; 51 | fast_assert!(x < y, "x ({}) should be less than y ({})", x, y); 52 | } 53 | 54 | #[test] 55 | #[should_panic] 56 | fn reported_location_simple() { 57 | verify_source_code_location(|| fast_assert!(100 < 0)); 58 | } 59 | 60 | #[test] 61 | #[should_panic] 62 | fn reported_location_custom_message() { 63 | let x = 100; 64 | let y = 0; 65 | verify_source_code_location(|| { 66 | fast_assert!(x < y, "x ({}) should be less than y ({})", x, y); 67 | }); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /fast_assert/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | /// A reimplementation of assert! that uses a closure to defer all 4 | /// panic-related work to the cold path. 5 | #[macro_export] 6 | macro_rules! fast_assert { 7 | // Rule 1: Handles calls with only a condition, like my_assert!(x == y). 8 | // It also accepts an optional trailing comma, like my_assert!(x == y,). 9 | ($cond:expr $(,)?) => { 10 | if !$cond { 11 | // If the condition is false, panic with a default message. 12 | // The stringify! macro converts the expression `$cond` into a string literal, 13 | // so the error message includes the exact code that failed. 14 | $crate::cold::assert_failed(|| { 15 | panic!("assertion failed: {}", stringify!($cond)); 16 | }); 17 | } 18 | }; 19 | // Rule 2: Handles calls with a condition and a custom message, 20 | // like my_assert!(x == y, "x should be equal to y, but was {}", x). 21 | ($cond:expr, $($arg:tt)+) => { 22 | if !$cond { 23 | // We pass a closure to the cold function. 24 | // No code inside this closure will be generated in the hot path. 25 | $crate::cold::assert_failed(|| { 26 | panic!($($arg)+); 27 | }); 28 | } 29 | }; 30 | } 31 | 32 | /// Private implementation detail. 33 | /// `pub` is required to make macros work from other crates, so stick #[doc(hidden)] on it. 34 | #[doc(hidden)] 35 | pub mod cold { 36 | /// This function is marked as `#[cold]` to hint to the compiler that it's 37 | /// rarely executed. The compiler uses this to optimize the call site, 38 | /// keeping the "hot path" (where the assertion succeeds) as lean as possible. 39 | /// 40 | /// This function is generic over a closure `F`. 41 | /// `F: FnOnce()` means it accepts any closure that can be called once 42 | /// and takes no arguments. 43 | /// 44 | /// The panic logic is provided by the caller via this closure. 45 | /// 46 | /// This doesn't need #[inline] because the function is generic 47 | /// and will be separately instantiated for each call site. 48 | #[cold] 49 | #[track_caller] 50 | pub fn assert_failed(msg_fn: F) 51 | where 52 | F: FnOnce(), 53 | { 54 | // We simply call the closure, which contains the panic!. 55 | msg_fn(); 56 | } 57 | } 58 | 59 | /// We only run basic sanity checks here. The really interesting tests are in a separate crate in this workspace. 60 | /// This is because getting macros to work when instantiated in the same file is easy, 61 | /// but getting them to work across crates is harder. 62 | #[cfg(test)] 63 | mod tests { 64 | use super::*; 65 | 66 | #[test] 67 | fn holds() { 68 | fast_assert!(0 < 100); 69 | } 70 | 71 | #[test] 72 | #[should_panic] 73 | fn fails() { 74 | fast_assert!(100 < 0); 75 | } 76 | 77 | #[test] 78 | fn holds_custom_message() { 79 | let x = 0; 80 | let y = 100; 81 | fast_assert!(x < y, "x ({}) should be less than y ({})", x, y); 82 | } 83 | 84 | #[test] 85 | #[should_panic] 86 | fn fails_custom_message() { 87 | let x = 100; 88 | let y = 0; 89 | fast_assert!(x < y, "x ({}) should be less than y ({})", x, y); 90 | } 91 | } 92 | --------------------------------------------------------------------------------