Skip to content

Commit

Permalink
WIP: return to the database-wrapping-storage setup
Browse files Browse the repository at this point in the history
  • Loading branch information
nikomatsakis committed Aug 4, 2024
1 parent 12e0741 commit 1b1e1fa
Show file tree
Hide file tree
Showing 16 changed files with 336 additions and 281 deletions.
52 changes: 51 additions & 1 deletion components/salsa-macros/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ struct DbMacro {
impl DbMacro {
fn try_db(self, input: syn::Item) -> syn::Result<TokenStream> {
match input {
syn::Item::Struct(input) => {
let has_storage_impl = self.has_storage_impl(&input)?;
Ok(quote! {
#has_storage_impl
#input
})
}
syn::Item::Trait(mut input) => {
self.add_salsa_view_method(&mut input)?;
Ok(quote! {
Expand All @@ -46,11 +53,54 @@ impl DbMacro {
}
_ => Err(syn::Error::new_spanned(
input,
"`db` must be applied to a trait or impl",
"`db` must be applied to a struct, trait, or impl",
)),
}
}

fn find_storage_field(&self, input: &syn::ItemStruct) -> syn::Result<syn::Ident> {
let storage = "storage";
for field in input.fields.iter() {
if let Some(i) = &field.ident {
if i == storage {
return Ok(i.clone());
}
} else {
return Err(syn::Error::new_spanned(
field,
"database struct must be a braced struct (`{}`) with a field named `storage`",
));
}
}

Err(syn::Error::new_spanned(
&input.ident,
"database struct must be a braced struct (`{}`) with a field named `storage`",
))
}

fn has_storage_impl(&self, input: &syn::ItemStruct) -> syn::Result<TokenStream> {
let storage = self.find_storage_field(input)?;
let db = &input.ident;
let zalsa = self.hygiene.ident("zalsa");

Ok(quote! {
const _: () = {
use salsa::plumbing as #zalsa;

unsafe impl #zalsa::HasStorage for #db {
fn storage(&self) -> &#zalsa::Storage<Self> {
&self.#storage
}

fn storage_mut(&mut self) -> &mut #zalsa::Storage<Self> {
&mut self.#storage
}
}
};
})
}

fn add_salsa_view_method(&self, input: &mut syn::ItemTrait) -> syn::Result<()> {
input.items.push(parse_quote! {
#[doc(hidden)]
Expand Down
18 changes: 9 additions & 9 deletions examples/calc/db.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
use std::sync::{Arc, Mutex};

use salsa::UserData;

pub type CalcDatabaseImpl = salsa::DatabaseImpl<Calc>;

// ANCHOR: db_struct
#[salsa::db]
#[derive(Default)]
pub struct Calc {
pub struct CalcDatabaseImpl {
storage: salsa::Storage<Self>,

// The logs are only used for testing and demonstrating reuse:
logs: Arc<Mutex<Option<Vec<String>>>>,
}
// ANCHOR_END: db_struct

impl Calc {
impl CalcDatabaseImpl {
/// Enable logging of each salsa event.
#[cfg(test)]
pub fn enable_logging(&self) {
Expand All @@ -34,12 +33,13 @@ impl Calc {
}

// ANCHOR: db_impl
impl UserData for Calc {
fn salsa_event(db: &CalcDatabaseImpl, event: &dyn Fn() -> salsa::Event) {
#[salsa::db]
impl salsa::Database for CalcDatabaseImpl {
fn salsa_event(&self, event: &dyn Fn() -> salsa::Event) {
let event = event();
eprintln!("Event: {event:?}");
// Log interesting events, if logging is enabled
if let Some(logs) = &mut *db.logs.lock().unwrap() {
if let Some(logs) = &mut *self.logs.lock().unwrap() {
// only log interesting events
if let salsa::EventKind::WillExecute { .. } = event.kind {
logs.push(format!("Event: {event:?}"));
Expand Down
20 changes: 12 additions & 8 deletions examples/lazy-input/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ use notify_debouncer_mini::{
notify::{RecommendedWatcher, RecursiveMode},
DebounceEventResult, Debouncer,
};
use salsa::{Accumulator, DatabaseImpl, Setter, UserData};
use salsa::{Accumulator, DatabaseImpl, Setter, Storage};

// ANCHOR: main
fn main() -> Result<()> {
// Create the channel to receive file change events.
let (tx, rx) = unbounded();
let mut db = DatabaseImpl::with(LazyInput::new(tx));
let mut db = LazyInputDatabase::new(tx);

let initial_file_path = std::env::args_os()
.nth(1)
Expand Down Expand Up @@ -74,34 +74,38 @@ trait Db: salsa::Database {
fn input(&self, path: PathBuf) -> Result<File>;
}

struct LazyInput {
#[salsa::db]
struct LazyInputDatabase {
storage: Storage<Self>,
logs: Mutex<Vec<String>>,
files: DashMap<PathBuf, File>,
file_watcher: Mutex<Debouncer<RecommendedWatcher>>,
}

impl LazyInput {
impl LazyInputDatabase {
fn new(tx: Sender<DebounceEventResult>) -> Self {
Self {
storage: Default::default(),
logs: Default::default(),
files: DashMap::new(),
file_watcher: Mutex::new(new_debouncer(Duration::from_secs(1), tx).unwrap()),
}
}
}

impl UserData for LazyInput {
fn salsa_event(db: &DatabaseImpl<Self>, event: &dyn Fn() -> salsa::Event) {
#[salsa::db]
impl salsa::Database for LazyInputDatabase {
fn salsa_event(&self, event: &dyn Fn() -> salsa::Event) {
// don't log boring events
let event = event();
if let salsa::EventKind::WillExecute { .. } = event.kind {
db.logs.lock().unwrap().push(format!("{:?}", event));
self.logs.lock().unwrap().push(format!("{:?}", event));
}
}
}

#[salsa::db]
impl Db for DatabaseImpl<LazyInput> {
impl Db for LazyInputDatabase {
fn input(&self, path: PathBuf) -> Result<File> {
let path = path
.canonicalize()
Expand Down
209 changes: 5 additions & 204 deletions src/database.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,11 @@
use std::{any::Any, marker::PhantomData, panic::RefUnwindSafe, sync::Arc};
use std::any::Any;

use parking_lot::{Condvar, Mutex};

use crate::{
self as salsa,
zalsa::Zalsa,
zalsa_local::{self, ZalsaLocal},
Durability, Event, EventKind, Revision,
};
use crate::{zalsa::ZalsaDatabase, Durability, Event, Revision};

/// The trait implemented by all Salsa databases.
/// You can create your own subtraits of this trait using the `#[salsa::db]` procedural macro.
///
/// # Safety
///
/// This trait can only safely be implemented by Salsa's [`DatabaseImpl`][] type.
///
/// FIXME: Document better the unsafety conditions we require.
#[salsa_macros::db]
pub unsafe trait Database: Send + AsDynDatabase + Any {
/// You can create your own subtraits of this trait using the `#[salsa::db]`(`crate::db`) procedural macro.
#[crate::db]
pub trait Database: Send + AsDynDatabase + Any + ZalsaDatabase {
/// This function is invoked by the salsa runtime at various points during execution.
/// You can customize what happens by implementing the [`UserData`][] trait.
/// By default, the event is logged at level debug using tracing facade.
Expand Down Expand Up @@ -58,21 +45,6 @@ pub unsafe trait Database: Send + AsDynDatabase + Any {
{
crate::attach::attach(self, || op(self))
}

/// Plumbing method: Access the internal salsa methods.
#[doc(hidden)]
fn zalsa(&self) -> &Zalsa;

/// Plumbing method: Access the internal salsa methods for mutating the database.
///
/// **WARNING:** Triggers a new revision, canceling other database handles.
/// This can lead to deadlock!
#[doc(hidden)]
fn zalsa_mut(&mut self) -> &mut Zalsa;

/// Access the thread-local state associated with this database
#[doc(hidden)]
fn zalsa_local(&self) -> &ZalsaLocal;
}

/// Upcast to a `dyn Database`.
Expand Down Expand Up @@ -108,174 +80,3 @@ impl dyn Database {
self.zalsa().views().try_view_as(self).unwrap()
}
}

/// Concrete implementation of the [`Database`][] trait.
/// Takes an optional type parameter `U` that allows you to thread your own data.
pub struct DatabaseImpl<U: UserData = ()> {
/// Reference to the database. This is always `Some` except during destruction.
zalsa_impl: Option<Arc<Zalsa>>,

/// Coordination data for cancellation of other handles when `zalsa_mut` is called.
/// This could be stored in Zalsa but it makes things marginally cleaner to keep it separate.
coordinate: Arc<Coordinate>,

/// Per-thread state
zalsa_local: zalsa_local::ZalsaLocal,

/// The `U` is stored as a `dyn Any` in `zalsa_impl`
phantom: PhantomData<U>,
}

impl<U: UserData + Default> Default for DatabaseImpl<U> {
fn default() -> Self {
Self::with(U::default())
}
}

impl DatabaseImpl<()> {
/// Create a new database with the given user data.
///
/// You can also use the [`Default`][] trait if your userdata implements it.
pub fn new() -> Self {
Self::with(())
}
}

impl<U: UserData> DatabaseImpl<U> {
/// Create a new database with the given user data.
///
/// You can also use the [`Default`][] trait if your userdata implements it.
pub fn with(u: U) -> Self {
Self {
zalsa_impl: Some(Arc::new(Zalsa::with(u))),
coordinate: Arc::new(Coordinate {
clones: Mutex::new(1),
cvar: Default::default(),
}),
zalsa_local: ZalsaLocal::new(),
phantom: PhantomData::<U>,
}
}

/// Access the `Arc<Zalsa>`. This should always be
/// possible as `zalsa_impl` only becomes
/// `None` once we are in the `Drop` impl.
fn zalsa_impl(&self) -> &Arc<Zalsa> {
self.zalsa_impl.as_ref().unwrap()
}

// ANCHOR: cancel_other_workers
/// Sets cancellation flag and blocks until all other workers with access
/// to this storage have completed.
///
/// This could deadlock if there is a single worker with two handles to the
/// same database!
fn cancel_others(&mut self) {
let zalsa = self.zalsa_impl();
zalsa.set_cancellation_flag();

self.salsa_event(&|| Event {
thread_id: std::thread::current().id(),

kind: EventKind::DidSetCancellationFlag,
});

let mut clones = self.coordinate.clones.lock();
while *clones != 1 {
self.coordinate.cvar.wait(&mut clones);
}
}
// ANCHOR_END: cancel_other_workers
}

impl<U: UserData> std::ops::Deref for DatabaseImpl<U> {
type Target = U;

fn deref(&self) -> &U {
self.zalsa_impl().user_data().downcast_ref::<U>().unwrap()
}
}

impl<U: UserData> std::ops::DerefMut for DatabaseImpl<U> {
fn deref_mut(&mut self) -> &mut U {
self.zalsa_mut()
.user_data_mut()
.downcast_mut::<U>()
.unwrap()
}
}

impl<U: UserData + RefUnwindSafe> RefUnwindSafe for DatabaseImpl<U> {}

#[salsa_macros::db]
unsafe impl<U: UserData> Database for DatabaseImpl<U> {
fn zalsa(&self) -> &Zalsa {
&**self.zalsa_impl()
}

fn zalsa_mut(&mut self) -> &mut Zalsa {
self.cancel_others();

// The ref count on the `Arc` should now be 1
let arc_zalsa_mut = self.zalsa_impl.as_mut().unwrap();
let zalsa_mut = Arc::get_mut(arc_zalsa_mut).unwrap();
zalsa_mut.new_revision();
zalsa_mut
}

fn zalsa_local(&self) -> &ZalsaLocal {
&self.zalsa_local
}

// Report a salsa event.
fn salsa_event(&self, event: &dyn Fn() -> Event) {
U::salsa_event(self, event)
}
}

impl<U: UserData> Clone for DatabaseImpl<U> {
fn clone(&self) -> Self {
*self.coordinate.clones.lock() += 1;

Self {
zalsa_impl: self.zalsa_impl.clone(),
coordinate: Arc::clone(&self.coordinate),
zalsa_local: ZalsaLocal::new(),
phantom: PhantomData::<U>,
}
}
}

impl<U: UserData> Drop for DatabaseImpl<U> {
fn drop(&mut self) {
// Drop the database handle *first*
self.zalsa_impl.take();

// *Now* decrement the number of clones and notify once we have completed
*self.coordinate.clones.lock() -= 1;
self.coordinate.cvar.notify_all();
}
}

pub trait UserData: Any + Sized + Send + Sync {
/// Callback invoked by the [`Database`][] at key points during salsa execution.
/// By overriding this method, you can inject logging or other custom behavior.
///
/// By default, the event is logged at level debug using the `tracing` crate.
///
/// # Parameters
///
/// * `event` a fn that, if called, will return the event that occurred
fn salsa_event(_db: &DatabaseImpl<Self>, event: &dyn Fn() -> Event) {
tracing::debug!("salsa_event: {:?}", event())
}
}

impl UserData for () {}

struct Coordinate {
/// Counter of the number of clones of actor. Begins at 1.
/// Incremented when cloned, decremented when dropped.
clones: Mutex<usize>,
cvar: Condvar,
}
Loading

0 comments on commit 1b1e1fa

Please sign in to comment.