use std::{ any::{Any, TypeId}, sync::Arc, }; use arc_swap::ArcSwap; use parking_lot::RwLock; use crate::zalsa::MemoIngredientIndex; #[derive(Default)] pub(crate) struct MemoTable { memos: RwLock>, } /// Wraps the data stored for a memoized entry. /// This struct has a customized Drop that will /// ensure that its `data` field is properly freed. #[derive(Default)] struct MemoEntry { data: Option, } /// Data for a memoized entry. /// This is a type-erased `Arc`, where `M` is the type of memo associated /// with that particular ingredient index. /// /// # Implementation note /// /// Every entry is associated with some ingredient that has been added to the database. /// That ingredient has a fixed type of values that it produces etc. /// Therefore, once a given entry goes from `Empty` to `Full`, /// the type-id associated with that entry should never change. /// /// We take advantage of this and use an `ArcSwap` to store the actual memo. /// This allows us to store into the memo-entry without acquiring a write-lock. /// However, using `ArcSwap` means we cannot use a `Arc` or any other wide pointer. /// Therefore, we hide the type by transmuting to `DummyMemo`; but we must then be very careful /// when freeing `MemoEntryData` values to transmute things back. See the `Drop` impl for /// [`MemoEntry`][] for details. struct MemoEntryData { /// The `type_id` of the erased memo type `M` type_id: TypeId, /// A pointer to `std::mem::drop::>` for the erased memo type `M` drop_fn: fn(Arc), /// An [`ArcSwap`][] to a `Arc` for the erased memo type `M` arc_swap: ArcSwap, } /// Dummy placeholder type that we use when erasing the memo type `M` in [`MemoEntryData`][]. enum DummyMemo {} impl MemoTable { fn to_dummy(memo: Arc) -> Arc { unsafe { std::mem::transmute::, Arc>(memo) } } unsafe fn from_dummy(memo: Arc) -> Arc { unsafe { std::mem::transmute::, Arc>(memo) } } fn drop_fn() -> fn(Arc) { let f: fn(Arc) = std::mem::drop::>; unsafe { std::mem::transmute::), fn(Arc)>(f) } } pub(crate) fn insert( &self, memo_ingredient_index: MemoIngredientIndex, memo: Arc, ) { // If the memo slot is already occupied, it must already have the // right type info etc, and we only need the read-lock. if let Some(MemoEntry { data: Some(MemoEntryData { type_id, drop_fn: _, arc_swap, }), }) = self.memos.read().get(memo_ingredient_index.as_usize()) { assert_eq!( *type_id, TypeId::of::(), "inconsistent type-id for `{memo_ingredient_index:?}`" ); arc_swap.store(Self::to_dummy(memo)); return; } // Otherwise we need the write lock. self.insert_cold(memo_ingredient_index, memo) } fn insert_cold( &self, memo_ingredient_index: MemoIngredientIndex, memo: Arc, ) { let mut memos = self.memos.write(); let memo_ingredient_index = memo_ingredient_index.as_usize(); memos.resize_with(memo_ingredient_index + 1, || MemoEntry::default()); memos[memo_ingredient_index] = MemoEntry { data: Some(MemoEntryData { type_id: TypeId::of::(), drop_fn: Self::drop_fn::(), arc_swap: ArcSwap::new(Self::to_dummy(memo)), }), }; } pub(crate) fn get( &self, memo_ingredient_index: MemoIngredientIndex, ) -> Option> { let memos = self.memos.read(); let Some(MemoEntry { data: Some(MemoEntryData { type_id, drop_fn: _, arc_swap, }), }) = memos.get(memo_ingredient_index.as_usize()) else { return None; }; assert_eq!( *type_id, TypeId::of::(), "inconsistent type-id for `{memo_ingredient_index:?}`" ); // SAFETY: type_id check asserted above unsafe { Some(Self::from_dummy(arc_swap.load_full())) } } } impl Drop for MemoEntry { fn drop(&mut self) { if let Some(MemoEntryData { type_id: _, drop_fn, arc_swap, }) = self.data.take() { let arc = arc_swap.into_inner(); drop_fn(arc); } } } impl Drop for DummyMemo { fn drop(&mut self) { unreachable!("should never get here") } }