debug_unwraps
Adds debug only unwrap functionality to Option and Result types for Rust
Motivation
When writing a new high-level structure use of .unwrap() and .expect() can
be useful to ensure that internal invariants are upheld. This allows unit and
integration tests to quickly fail if a code-change violates some expected
structure.
On the other hand, in release mode it would be nice to strip such checks because
it is expected that the API itself will enforce the invariant.
Consider the following example structure:
struct NumTreeBuilder {
stack: Vec<usize>,
nodes: Vec<Vec<usize>>,
data: Vec<Vec<u32>>,
}
impl NumTreeBuilder {
pub fn new() -> Self {
Self {
stack: vec![0],
nodes: vec![Vec::new()],
names: vec![Vec::new()],
}
}
pub fn add_num(&mut self, num: u32) {
if let Some(current) = self.stack.last() {
self.data.get_mut(*current)
.expect("A node ID was on the stack but didn't have associated data")
.push(num);
} else {
unreachable!("The stack should never be empty because the root cannot be popped");
}
}
pub fn start_child(&mut self) {
let child_id = self.nodes.len();
self.nodes.push(Vec::new());
self.data.push(Vec::new());
if let Some(current) = self.stack.last() {
self.nodes.get_mut(*current)
.expect("A node ID was on the stack but didn't have associated data")
.push(child_id);
self.stack.push(child_id);
} else {
unreachable!("The stack should never be empty because the root cannot be popped");
}
}
pub fn finish_child(&mut self) {
if self.stack.len() > 1 {
self.stack.pop();
}
}
}
The API is designed to not allow invalid states such as an empty stack or
missing node data, but we are forced to always do run-time checks.
Currently the only way to get debug only checks is to use debug_assert!():
impl NumTreeBuilder {
pub fn start_child(&mut self) {
let child_id = self.nodes.len();
self.nodes.push(Vec::new());
self.data.push(Vec::new());
debug_assert!(self.stack.last().is_some(), "The stack should never be empty because the root cannot be popped");
let current = unsafe {self.stack.last().unwrap_unchecked()};
debug_assert!(self.nodes.get_mut(*current).is_some(), "A node ID was on the stack but didn't have associated data");
unsafe { self.nodes.get_mut(*current).unwrap_uncheckd().push(child_id) };
self.stack.push(child_id);
}
}
This has some disadvantages because it separates the error check from the location
that the error can be generated. Additionally if each operation is in its own
unsafe block it increases the number of locations to audit when checking for
crate safety.
Solution
This crate provides extension traits for Option<T> and Result<T,E> which
conditionally enable debugging only when compiled with debug-assertions.
impl NumTreeBuilder {
pub fn start_child(&mut self) {
use debug_unwraps::DebugUnwrapExt;
let child_id = self.nodes.len();
self.nodes.push(Vec::new());
self.data.push(Vec::new());
unsafe {
let current = self.stack.last().debug_expect_unchecked("The stack should never be empty because the root cannot be popped");
self.nodes.get_mut(*current)
.debug_expect_unchecked("A node ID was on the stack but didn't have associated data")
.push(child_id);
}
self.stack.push(child_id);
}
}
With this code, the error checking is once again inline and failures during
refactorign can be caught during unit/integration tests. But in Release the
code will not bother checking.