use std::{convert::Infallible, error::Error, sync::PoisonError};
use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
pub trait HandlePoisonResult {
type PoisonlessResult;
#[must_use]
fn ignore_poison(self) -> Self::PoisonlessResult;
fn panic_if_poison(self) -> Self::PoisonlessResult;
}
#[inline]
fn prove_unreachable(poison: &PoisonError<Infallible>) -> ! {
#[expect(clippy::uninhabited_references, reason = "this function is not reachable")]
match *poison.get_ref() {}
}
pub type LockResult<T> = Result<T, LockError<T>>;
pub type PoisonlessLockResult<T> = Result<T, LockError<Infallible>>;
impl<T> HandlePoisonResult for LockResult<T> {
type PoisonlessResult = PoisonlessLockResult<T>;
#[inline]
fn ignore_poison(self) -> Self::PoisonlessResult {
match self.map_err(LockError::ignore_poison) {
Ok(t) => Ok(t),
Err(poisonless_result) => poisonless_result,
}
}
#[inline]
fn panic_if_poison(self) -> Self::PoisonlessResult {
self.map_err(LockError::panic_if_poison)
}
}
pub enum LockError<T> {
Poisoned(PoisonError<T>),
LockedByCurrentThread,
}
impl<T> LockError<T> {
#[inline]
pub fn ignore_poison(self) -> PoisonlessLockResult<T> {
match self {
Self::Poisoned(poison) => Ok(poison.into_inner()),
Self::LockedByCurrentThread => Err(LockError::LockedByCurrentThread),
}
}
#[inline]
#[must_use]
pub fn panic_if_poison(self) -> LockError<Infallible> {
match self {
#[expect(
clippy::panic,
reason = "library users will frequently want to panic on poison",
)]
Self::Poisoned(_) => panic!("LockError was poison"),
Self::LockedByCurrentThread => LockError::LockedByCurrentThread,
}
}
}
impl<T> From<PoisonError<T>> for LockError<T> {
#[inline]
fn from(poison: PoisonError<T>) -> Self {
Self::Poisoned(poison)
}
}
impl<T> Debug for LockError<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
Self::Poisoned(poison) => f.debug_tuple("Poisoned").field(&poison).finish(),
Self::LockedByCurrentThread => f.write_str("LockedByCurrentThread"),
}
}
}
impl<T> Display for LockError<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
Self::Poisoned(_) => write!(
f,
"LockError due to poison (another thread panicked)",
),
Self::LockedByCurrentThread => write!(
f,
"Failed to acquire a lock, because the same thread was holding it",
),
}
}
}
impl<T> Error for LockError<T> {}
impl PartialEq for LockError<Infallible> {
#[inline]
fn eq(&self, _other: &Self) -> bool {
match self {
Self::LockedByCurrentThread => true,
Self::Poisoned(poison) => prove_unreachable(poison),
}
}
}
impl Eq for LockError<Infallible> {}
pub type TryLockResult<T> = Result<T, TryLockError<T>>;
pub type PoisonlessTryLockResult<T> = Result<T, TryLockError<Infallible>>;
impl<T> HandlePoisonResult for TryLockResult<T> {
type PoisonlessResult = PoisonlessTryLockResult<T>;
#[inline]
fn ignore_poison(self) -> Self::PoisonlessResult {
match self.map_err(TryLockError::ignore_poison) {
Ok(t) => Ok(t),
Err(poisonless_result) => poisonless_result,
}
}
#[inline]
fn panic_if_poison(self) -> Self::PoisonlessResult {
self.map_err(TryLockError::panic_if_poison)
}
}
pub enum TryLockError<T> {
Poisoned(PoisonError<T>),
LockedByCurrentThread,
WouldBlock,
}
impl<T> TryLockError<T> {
#[inline]
pub fn ignore_poison(self) -> PoisonlessTryLockResult<T> {
match self {
Self::Poisoned(poison) => Ok(poison.into_inner()),
Self::LockedByCurrentThread => Err(TryLockError::LockedByCurrentThread),
Self::WouldBlock => Err(TryLockError::WouldBlock),
}
}
#[inline]
#[must_use]
pub fn panic_if_poison(self) -> TryLockError<Infallible> {
match self {
#[expect(
clippy::panic,
reason = "library users will frequently want to panic on poison",
)]
Self::Poisoned(_) => panic!("TryLockError was poison"),
Self::LockedByCurrentThread => TryLockError::LockedByCurrentThread,
Self::WouldBlock => TryLockError::WouldBlock,
}
}
}
impl<T> From<PoisonError<T>> for TryLockError<T> {
#[inline]
fn from(poison: PoisonError<T>) -> Self {
Self::Poisoned(poison)
}
}
impl<T> Debug for TryLockError<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
Self::Poisoned(poison) => f.debug_tuple("Poisoned").field(&poison).finish(),
Self::LockedByCurrentThread => f.write_str("LockedByCurrentThread"),
Self::WouldBlock => f.write_str("WouldBlock"),
}
}
}
impl<T> Display for TryLockError<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
Self::Poisoned(_) => write!(
f,
"TryLockError due to poison (another thread panicked)",
),
Self::LockedByCurrentThread => write!(
f,
"Failed to acquire a lock, because the same thread was holding it",
),
Self::WouldBlock => write!(
f,
"Lock was held by a different thread, so acquiring it would block",
),
}
}
}
impl<T> Error for TryLockError<T> {}
impl PartialEq for TryLockError<Infallible> {
#[inline]
fn eq(&self, other: &Self) -> bool {
match self {
Self::LockedByCurrentThread => matches!(other, Self::LockedByCurrentThread),
Self::WouldBlock => matches!(other, Self::WouldBlock),
Self::Poisoned(poison) => prove_unreachable(poison),
}
}
}
impl Eq for TryLockError<Infallible> {}
pub type AccessResult<T> = Result<T, AccessError<T>>;
pub type PoisonlessAccessResult<T> = Result<T, AccessError<Infallible>>;
impl<T> HandlePoisonResult for AccessResult<T> {
type PoisonlessResult = PoisonlessAccessResult<T>;
#[inline]
fn ignore_poison(self) -> Self::PoisonlessResult {
match self.map_err(AccessError::ignore_poison) {
Ok(t) => Ok(t),
Err(poisonless_result) => poisonless_result,
}
}
#[inline]
fn panic_if_poison(self) -> Self::PoisonlessResult {
self.map_err(|err| AccessError::panic_if_poison(err))
}
}
pub struct AccessError<T> {
pub poison: PoisonError<T>,
}
impl<T> AccessError<T> {
#[expect(clippy::missing_errors_doc, reason = "the function is infallible")]
#[inline]
pub fn ignore_poison(self) -> PoisonlessAccessResult<T> {
Ok(self.poison.into_inner())
}
#[inline]
pub fn panic_if_poison(self) -> ! {
#![expect(
clippy::panic,
reason = "library users will frequently want to panic on poison",
)]
panic!("AccessError is poison")
}
}
impl<T> From<PoisonError<T>> for AccessError<T> {
#[inline]
fn from(poison: PoisonError<T>) -> Self {
Self { poison }
}
}
impl<T> Debug for AccessError<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
f.debug_struct("AccessError")
.field("poison", &self.poison)
.finish()
}
}
impl<T> Display for AccessError<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(f, "AccessError due to poison (another thread panicked)")
}
}
impl<T> Error for AccessError<T> {}
impl PartialEq for AccessError<Infallible> {
#[inline]
fn eq(&self, _other: &Self) -> bool {
prove_unreachable(&self.poison)
}
}
impl Eq for AccessError<Infallible> {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lock_ignore_poison() {
let res_o: LockResult<()> = Ok(());
assert!(matches!(res_o.ignore_poison(), Ok(())));
let res_e: LockResult<()> = Err(LockError::LockedByCurrentThread);
assert!(matches!(res_e.ignore_poison(), Err(LockError::LockedByCurrentThread)));
let res_p: LockResult<()> = Err(PoisonError::new(()).into());
assert!(matches!(res_p.ignore_poison(), Ok(())));
}
#[test]
fn lock_panic_if_poison() {
let res_o: LockResult<()> = Ok(());
assert!(matches!(res_o.panic_if_poison(), Ok(())));
let res_e: LockResult<()> = Err(LockError::LockedByCurrentThread);
assert!(matches!(res_e.panic_if_poison(), Err(LockError::LockedByCurrentThread)));
}
#[test]
#[should_panic = "LockError was poison"]
fn panicking_lock_panic_if_poison() {
let res_p: LockResult<()> = Err(PoisonError::new(()).into());
#[expect(
clippy::let_underscore_must_use,
clippy::let_underscore_untyped,
reason = "function never returns",
)]
let _ = res_p.panic_if_poison();
}
#[test]
fn try_lock_ignore_poison() {
let res_o: TryLockResult<()> = Ok(());
assert!(matches!(res_o.ignore_poison(), Ok(())));
let res_e: TryLockResult<()> = Err(TryLockError::LockedByCurrentThread);
assert!(matches!(res_e.ignore_poison(), Err(TryLockError::LockedByCurrentThread)));
let res_p: TryLockResult<()> = Err(PoisonError::new(()).into());
assert!(matches!(res_p.ignore_poison(), Ok(())));
}
#[test]
fn try_lock_panic_if_poison() {
let res_o: TryLockResult<()> = Ok(());
assert!(matches!(res_o.panic_if_poison(), Ok(())));
let res_e: TryLockResult<()> = Err(TryLockError::LockedByCurrentThread);
assert!(matches!(res_e.panic_if_poison(), Err(TryLockError::LockedByCurrentThread)));
}
#[test]
#[should_panic = "TryLockError was poison"]
fn panicking_try_lock_panic_if_poison() {
let res_p: TryLockResult<()> = Err(PoisonError::new(()).into());
#[expect(
clippy::let_underscore_must_use,
clippy::let_underscore_untyped,
reason = "function never returns",
)]
let _ = res_p.panic_if_poison();
}
#[test]
fn access_ignore_poison() {
let res_o: AccessResult<()> = Ok(());
assert!(matches!(res_o.ignore_poison(), Ok(())));
let res_p: AccessResult<()> = Err(PoisonError::new(()).into());
assert!(matches!(res_p.ignore_poison(), Ok(())));
}
#[test]
fn access_panic_if_poison() {
let res_o: AccessResult<()> = Ok(());
assert!(matches!(res_o.panic_if_poison(), Ok(())));
}
#[test]
#[should_panic = "AccessError is poison"]
fn panicking_access_panic_if_poison() {
let res_p: AccessResult<()> = Err(PoisonError::new(()).into());
#[expect(
clippy::let_underscore_must_use,
clippy::let_underscore_untyped,
reason = "function never returns",
)]
let _ = res_p.panic_if_poison();
}
fn test_eq_impl<E: Eq, const N: usize>(errors: &[E; N]) {
for (i, error) in errors.iter().enumerate() {
for (j, other) in errors.iter().enumerate() {
assert_eq!(i == j, error == other);
}
}
}
#[test]
fn eq_impls() {
test_eq_impl(&[
LockError::<Infallible>::LockedByCurrentThread,
]);
test_eq_impl(&[
TryLockError::<Infallible>::LockedByCurrentThread,
TryLockError::<Infallible>::WouldBlock,
]);
}
}