use alloc::{
boxed::Box,
collections::BTreeMap,
string::String,
sync::{Arc, Weak},
};
use core::{
fmt,
ops::Deref,
sync::atomic::{AtomicBool, AtomicUsize, Ordering},
};
pub trait IdAllocator: Send + Sync + 'static {
fn lease(&self) -> Result<String, IdsExhausted>;
fn release(&self, word: &str);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("every model-facing id is leased")]
pub struct IdsExhausted;
#[derive(Clone)]
pub struct AnyIdAllocator(Arc<dyn IdAllocator>);
impl AnyIdAllocator {
pub fn new(allocator: impl IdAllocator) -> Self {
Self(Arc::new(allocator))
}
pub fn lease(&self) -> Result<IdLease, IdsExhausted> {
let word = self.0.lease()?;
assert!(
is_word(&word),
"the id allocator handed out '{word}', which is not one or more lowercase ASCII letters"
);
Ok(IdLease(Arc::new(Held {
word,
allocator: self.clone(),
})))
}
}
fn is_word(word: &str) -> bool {
!word.is_empty() && word.bytes().all(|byte| byte.is_ascii_lowercase())
}
impl Default for AnyIdAllocator {
fn default() -> Self {
Self::new(WordAllocator::default())
}
}
impl fmt::Debug for AnyIdAllocator {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AnyIdAllocator").finish_non_exhaustive()
}
}
#[derive(Clone)]
pub struct IdLease(Arc<Held>);
struct Held {
word: String,
allocator: AnyIdAllocator,
}
impl Drop for Held {
fn drop(&mut self) {
self.allocator.0.release(&self.word);
}
}
impl IdLease {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0.word
}
#[must_use]
pub fn same_lease(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
#[must_use]
pub fn downgrade(&self) -> WeakIdLease {
WeakIdLease(Arc::downgrade(&self.0))
}
}
#[derive(Clone)]
pub struct WeakIdLease(Weak<Held>);
impl WeakIdLease {
#[must_use]
pub fn upgrade(&self) -> Option<IdLease> {
self.0.upgrade().map(IdLease)
}
#[must_use]
pub fn refers_to(&self, lease: &IdLease) -> bool {
core::ptr::eq(self.0.as_ptr(), Arc::as_ptr(&lease.0))
}
}
impl fmt::Debug for WeakIdLease {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("WeakIdLease")
}
}
impl Deref for IdLease {
type Target = str;
fn deref(&self) -> &str {
self.as_str()
}
}
impl AsRef<str> for IdLease {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for IdLease {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl fmt::Debug for IdLease {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("IdLease").field(&self.as_str()).finish()
}
}
const ENGLISH_WORDS: &str = include_str!("ids/english.txt");
pub struct WordAllocator {
words: Box<[Box<str>]>,
positions: BTreeMap<Box<str>, usize>,
leased: Box<[AtomicBool]>,
cursor: AtomicUsize,
}
impl WordAllocator {
pub fn new<W: Into<Box<str>>>(words: impl IntoIterator<Item = W>) -> Self {
let words: Box<[Box<str>]> = words.into_iter().map(Into::into).collect();
assert!(!words.is_empty(), "a word allocator needs words");
let mut positions = BTreeMap::new();
for (position, word) in words.iter().enumerate() {
assert!(
is_word(word),
"'{word}' is not one or more lowercase ASCII letters"
);
assert!(
positions.insert(word.clone(), position).is_none(),
"the word '{word}' is listed twice"
);
}
let leased = words.iter().map(|_| AtomicBool::new(false)).collect();
Self {
words,
positions,
leased,
cursor: AtomicUsize::new(0),
}
}
}
impl Default for WordAllocator {
fn default() -> Self {
Self::new(ENGLISH_WORDS.lines())
}
}
impl fmt::Debug for WordAllocator {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("WordAllocator")
.field("words", &self.words.len())
.finish_non_exhaustive()
}
}
impl IdAllocator for WordAllocator {
fn lease(&self) -> Result<String, IdsExhausted> {
let count = self.words.len();
for _ in 0..count {
let position = self.cursor.fetch_add(1, Ordering::Relaxed) % count;
if !self.leased[position].swap(true, Ordering::AcqRel) {
return Ok(String::from(&*self.words[position]));
}
}
Err(IdsExhausted)
}
fn release(&self, word: &str) {
let position = *self
.positions
.get(word)
.unwrap_or_else(|| panic!("released '{word}', which this allocator never leased"));
assert!(
self.leased[position].swap(false, Ordering::AcqRel),
"released '{word}', which is not leased"
);
}
}
#[cfg(test)]
mod tests {
use alloc::string::ToString;
use super::*;
#[test]
fn the_default_list_is_single_lowercase_words() {
assert_eq!(ENGLISH_WORDS.lines().count(), 2048);
let _ = WordAllocator::default();
}
#[test]
#[should_panic(expected = "is not one or more lowercase ASCII letters")]
fn a_word_of_another_shape_is_refused() {
let _ = WordAllocator::new(["river", "task-1"]);
}
struct Numbered;
impl IdAllocator for Numbered {
fn lease(&self) -> Result<String, IdsExhausted> {
Ok("7".to_string())
}
fn release(&self, _word: &str) {}
}
#[test]
#[should_panic(expected = "handed out '7'")]
fn an_embedder_word_of_another_shape_fails_the_lease() {
let _ = AnyIdAllocator::new(Numbered).lease();
}
#[test]
fn live_leases_never_share_a_word_and_a_released_word_comes_back() {
let ids = AnyIdAllocator::new(WordAllocator::new(["river", "stone"]));
let river = ids.lease().expect("a free word");
let stone = ids.lease().expect("a free word");
assert_eq!((river.as_str(), stone.as_str()), ("river", "stone"));
assert_eq!(ids.lease().unwrap_err(), IdsExhausted);
drop(river);
assert_eq!(
ids.lease().expect("river is free again").to_string(),
"river"
);
}
#[test]
fn a_lease_ends_with_its_last_clone() {
let ids = AnyIdAllocator::new(WordAllocator::new(["river"]));
let lease = ids.lease().expect("a free word");
let held = lease.clone();
drop(lease);
assert_eq!(
ids.lease().unwrap_err(),
IdsExhausted,
"a clone still holds it"
);
drop(held);
assert_eq!(
&*ids.lease().expect("released with the last clone"),
"river"
);
}
#[test]
fn only_clones_are_the_same_lease() {
let ids = AnyIdAllocator::new(WordAllocator::new(["river"]));
let lease = ids.lease().expect("a free word");
assert!(lease.same_lease(&lease.clone()));
drop(lease);
let again = ids.lease().expect("river is free again");
let other = AnyIdAllocator::new(WordAllocator::new(["river"]))
.lease()
.expect("a free word");
assert_eq!(again.as_str(), other.as_str());
assert!(!again.same_lease(&other), "a word alone is not the lease");
let weak = again.downgrade();
assert!(weak.refers_to(&again));
assert!(!weak.refers_to(&other));
drop(again);
let after = ids.lease().expect("river is free again");
assert!(
!weak.refers_to(&after),
"a released lease is not its successor"
);
}
#[test]
fn a_weak_lease_upgrades_only_while_the_word_is_held() {
let ids = AnyIdAllocator::new(WordAllocator::new(["river"]));
let lease = ids.lease().expect("a free word");
let weak = lease.downgrade();
assert_eq!(weak.upgrade().as_deref(), Some("river"));
drop(lease);
assert!(weak.upgrade().is_none(), "the word went back");
let again = ids.lease().expect("river is free again");
assert!(
weak.upgrade().is_none(),
"a word leased again names something else: {again}"
);
}
#[test]
#[should_panic(expected = "is listed twice")]
fn a_word_listed_twice_is_refused() {
let _ = WordAllocator::new(["river", "river"]);
}
#[test]
#[should_panic(expected = "which is not leased")]
fn releasing_a_free_word_is_a_bug() {
WordAllocator::new(["river"]).release("river");
}
}