#[derive(Clone, PartialEq, Debug)]
pub struct BorrowState {
shared_count: usize,
mut_count: usize,
inaccessible_count: usize,
poisoned: bool,
}
impl BorrowState {
pub fn new() -> Self {
Self {
shared_count: 0,
mut_count: 0,
inaccessible_count: 0,
poisoned: false,
}
}
pub fn has_accessible(&self) -> bool {
let count = self.mut_count - self.inaccessible_count;
assert!(
count <= 1,
"there should never be more than 1 accessible mutable reference"
);
count == 1
}
pub fn shared_count(&self) -> usize {
self.shared_count
}
pub fn mut_count(&self) -> usize {
self.mut_count
}
pub fn is_poisoned(&self) -> bool {
self.poisoned
}
pub(crate) fn poison(&mut self, err: impl Into<String>) -> Result<(), BorrowStateErr> {
self.poisoned = true;
Err(BorrowStateErr::Poisoned(err.into()))
}
fn ensure_not_poisoned(&self) -> Result<(), BorrowStateErr> {
if self.is_poisoned() {
return Err(BorrowStateErr::IsPoisoned);
}
Ok(())
}
fn ensure_can_ref(&self) -> Result<(), BorrowStateErr> {
self.ensure_not_poisoned()?;
if self.has_accessible() {
return Err("cannot borrow while accessible mutable borrow exists".into());
}
Ok(())
}
fn ensure_can_mut_ref(&self) -> Result<(), BorrowStateErr> {
self.ensure_not_poisoned()?;
if self.has_accessible() {
return Err("cannot borrow while accessible mutable borrow exists".into());
}
if self.shared_count != 0 {
return Err("cannot borrow mutable while shared borrow exists".into());
}
Ok(())
}
pub fn increment_shared(&mut self) -> Result<usize, BorrowStateErr> {
self.ensure_not_poisoned()?;
self.ensure_can_ref()?;
self.shared_count = self
.shared_count
.checked_add(1)
.ok_or("could not increment shared count")?;
Ok(self.shared_count)
}
pub fn decrement_shared(&mut self) -> Result<usize, BorrowStateErr> {
self.ensure_not_poisoned()?;
if self.shared_count == 0 {
return Err("cannot decrement shared counter when no shared reference exists".into());
}
if self.has_accessible() {
self.poison("shared reference tracked while accessible mutable reference exists")?;
}
self.shared_count -= 1;
Ok(self.shared_count)
}
pub fn increment_mut(&mut self) -> Result<usize, BorrowStateErr> {
self.ensure_not_poisoned()?;
self.ensure_can_mut_ref()?;
self.mut_count = self
.mut_count
.checked_add(1)
.ok_or("could not increment mut count")?;
Ok(self.mut_count)
}
pub fn decrement_mut(&mut self) -> Result<usize, BorrowStateErr> {
self.ensure_not_poisoned()?;
if self.mut_count == 0 {
return Err("cannot decrement mutable counter when no mutable reference exists".into());
}
if self.mut_count == self.inaccessible_count {
return Err(
"cannot decrement mutable counter when current mutable reference is inaccessible"
.into(),
);
}
if self.mut_count - 1 != self.inaccessible_count {
self.poison("`inaccessible_count` does not fit its invariant")?;
}
self.mut_count -= 1;
Ok(self.mut_count)
}
pub fn set_inaccessible(&mut self) -> Result<usize, BorrowStateErr> {
if !self.has_accessible() {
return Err(
"cannot set current reference as inaccessible when no accessible reference exists"
.into(),
);
}
self.inaccessible_count = self
.inaccessible_count
.checked_add(1)
.ok_or("could not increment inaccessible count")?;
Ok(self.inaccessible_count)
}
pub(crate) fn may_unset_inaccessible(&self) -> bool {
!self.has_accessible() && self.shared_count() == 0 && self.inaccessible_count > 0
}
pub fn unset_inaccessible(&mut self) -> Result<usize, BorrowStateErr> {
if self.has_accessible() {
return Err("cannot set current reference as accessible when an accessible mutable reference already exists".into());
}
if self.shared_count() > 0 {
return Err(
"cannot set current reference as accessible when a shared reference exists".into(),
);
}
if self.inaccessible_count == 0 {
return Err(
"cannot mark mut pointer as accessible when there are no inaccessible pointers"
.into(),
);
}
self.inaccessible_count = self
.inaccessible_count
.checked_sub(1)
.ok_or("could not decrement inaccessible count")?;
Ok(self.inaccessible_count)
}
}
impl Default for BorrowState {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Eq, PartialEq, Debug)]
pub enum BorrowStateErr {
Poisoned(String),
IsPoisoned,
Custom(String),
}
impl std::fmt::Display for BorrowStateErr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BorrowStateErr::Poisoned(err) => write!(f, "the borrow state was poisoned: {err}"),
BorrowStateErr::IsPoisoned => write!(f, "the borrow state is poisoned"),
BorrowStateErr::Custom(err) => f.write_str(err),
}
}
}
impl std::error::Error for BorrowStateErr {}
impl<'a> From<&'a str> for BorrowStateErr {
fn from(value: &'a str) -> Self {
Self::Custom(value.to_string())
}
}
impl From<String> for BorrowStateErr {
fn from(value: String) -> Self {
Self::Custom(value)
}
}
#[cfg(test)] #[cfg_attr(published_docs, doc(cfg(test)))]
mod test {
use std::collections::HashSet;
use super::*;
type State = (usize, usize, usize);
const MAX_COUNT: usize = 2;
struct Op {
name: &'static str,
method: fn(&mut BorrowState) -> Result<usize, BorrowStateErr>,
expected_success: fn(&BorrowState) -> bool,
expected_change: fn(&mut BorrowState),
expected_result: fn(&BorrowState) -> usize,
}
const OPS: &[Op] = &[
Op {
name: "increment_shared",
method: BorrowState::increment_shared,
expected_success: |s| !s.has_accessible(),
expected_change: |s| s.shared_count += 1,
expected_result: |s| s.shared_count,
},
Op {
name: "decrement_shared",
method: BorrowState::decrement_shared,
expected_success: |s| s.shared_count > 0,
expected_change: |s| s.shared_count -= 1,
expected_result: |s| s.shared_count,
},
Op {
name: "increment_mut",
method: BorrowState::increment_mut,
expected_success: |s| !s.has_accessible() && s.shared_count == 0,
expected_change: |s| s.mut_count += 1,
expected_result: |s| s.mut_count,
},
Op {
name: "decrement_mut",
method: BorrowState::decrement_mut,
expected_success: |s| s.has_accessible(),
expected_change: |s| s.mut_count -= 1,
expected_result: |s| s.mut_count,
},
Op {
name: "set_inaccessible",
method: BorrowState::set_inaccessible,
expected_success: |s| s.has_accessible(),
expected_change: |s| s.inaccessible_count += 1,
expected_result: |s| s.inaccessible_count,
},
Op {
name: "unset_inaccessible",
method: BorrowState::unset_inaccessible,
expected_success: |s| {
!s.has_accessible() && s.shared_count == 0 && s.inaccessible_count > 0
},
expected_change: |s| s.inaccessible_count -= 1,
expected_result: |s| s.inaccessible_count,
},
];
fn build((shared, mutable, inaccessible): State) -> BorrowState {
BorrowState {
shared_count: shared,
mut_count: mutable,
inaccessible_count: inaccessible,
poisoned: false,
}
}
#[test]
fn exhaustive_model_check() {
let mut visited = HashSet::from([(0, 0, 0)]);
let mut queue = vec![(0, 0, 0)];
while let Some(state) = queue.pop() {
let initial = build(state);
assert!(
!initial.is_poisoned(),
"reachable state {state:?} is poisoned"
);
assert!(
initial.mut_count - initial.inaccessible_count <= 1,
"more than one accessible mut: {state:?}"
);
assert!(
!(initial.shared_count > 0 && initial.has_accessible()),
"shared and accessible mut coexist: {state:?}"
);
for op in OPS {
let name = op.name;
let mut actual = initial.clone();
let result = (op.method)(&mut actual);
let succeeded = result.is_ok();
assert_eq!(
succeeded,
(op.expected_success)(&initial),
"`{name}` success mismatch in {state:?}"
);
let mut expected = initial.clone();
if succeeded {
(op.expected_change)(&mut expected);
let expected_result = Ok((op.expected_result)(&expected));
assert_eq!(
result, expected_result,
"`{name}` wrong return value in {state:?}"
);
}
assert_eq!(
actual, expected,
"`{name}` produced wrong state in {state:?}"
);
assert!(!actual.is_poisoned(), "`{name}` poisoned state {state:?}");
let next = (
actual.shared_count,
actual.mut_count,
actual.inaccessible_count,
);
if succeeded && next.0 <= MAX_COUNT && next.1 <= MAX_COUNT && visited.insert(next) {
queue.push(next);
}
}
}
}
#[test]
fn poisoned_unset_shared_ref() {
let mut state = BorrowState::new();
assert!(!state.is_poisoned());
for step in [
BorrowState::increment_mut,
BorrowState::set_inaccessible,
BorrowState::increment_shared,
BorrowState::unset_inaccessible,
BorrowState::increment_shared,
BorrowState::decrement_shared,
] {
_ = step(&mut state);
assert!(!state.is_poisoned());
}
}
}