use std::{ any::{Any, TypeId}, fmt::Debug, sync::Arc, }; use arc_swap::ArcSwap; use parking_lot::RwLock; use crate::{zalsa::MemoIngredientIndex, zalsa_local::QueryOrigin}; /// The "memo table" stores the memoized results of tracked function calls. /// Every tracked function must take a salsa struct as its first argument /// and memo tables are attached to those salsa structs as auxiliary data. #[derive(Default)] pub(crate) struct MemoTable { memos: RwLock>, } pub(crate) trait Memo: Any + Send + Sync + Debug { /// Returns the `origin` of this memo fn origin(&self) -> &QueryOrigin; } /// 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` to_dyn_fn: fn(Arc) -> 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`][]. struct 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 to_dyn_fn() -> fn(Arc) -> Arc { let f: fn(Arc) -> Arc = |x| x; unsafe { std::mem::transmute::) -> Arc, fn(Arc) -> Arc>( f, ) } } pub(crate) fn insert( &self, memo_ingredient_index: MemoIngredientIndex, memo: Arc, ) -> Option> { // 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, to_dyn_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:?}`" ); let old_memo = arc_swap.swap(Self::to_dummy(memo)); return unsafe { Some(Self::from_dummy(old_memo)) }; } // Otherwise we need the write lock. self.insert_cold(memo_ingredient_index, memo) } fn insert_cold( &self, memo_ingredient_index: MemoIngredientIndex, memo: Arc, ) -> Option> { let mut memos = self.memos.write(); let memo_ingredient_index = memo_ingredient_index.as_usize(); if memos.len() < memo_ingredient_index + 1 { memos.resize_with(memo_ingredient_index + 1, MemoEntry::default); } let old_entry = std::mem::replace( &mut memos[memo_ingredient_index].data, Some(MemoEntryData { type_id: TypeId::of::(), to_dyn_fn: Self::to_dyn_fn::(), arc_swap: ArcSwap::new(Self::to_dummy(memo)), }), ); old_entry.map(|MemoEntryData { type_id: _, to_dyn_fn: _, arc_swap, }| unsafe { Self::from_dummy(arc_swap.into_inner()) }) } pub(crate) fn get( &self, memo_ingredient_index: MemoIngredientIndex, ) -> Option> { let memos = self.memos.read(); let Some(MemoEntry { data: Some(MemoEntryData { type_id, to_dyn_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())) } } pub(crate) fn into_memos( mut self, ) -> impl Iterator)> { let memos = std::mem::take(self.memos.get_mut()); memos .into_iter() .zip(0..) .filter_map(|(mut memo, index)| memo.data.take().map(|d| (d, index))) .map( |( MemoEntryData { type_id: _, to_dyn_fn, arc_swap, }, index, )| { ( MemoIngredientIndex::from_usize(index), to_dyn_fn(arc_swap.into_inner()), ) }, ) } } impl Drop for MemoEntry { fn drop(&mut self) { if let Some(MemoEntryData { type_id: _, to_dyn_fn, arc_swap, }) = self.data.take() { let arc = arc_swap.into_inner(); std::mem::drop(to_dyn_fn(arc)); } } } impl Drop for DummyMemo { fn drop(&mut self) { unreachable!("should never get here") } } impl std::fmt::Debug for MemoTable { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("MemoTable").finish() } }