diff --git a/book/src/common_patterns.md b/book/src/common_patterns.md index 1f8c4f1e..439dabc6 100644 --- a/book/src/common_patterns.md +++ b/book/src/common_patterns.md @@ -1 +1,52 @@ # Common patterns + +## On Demnd (Lazy) Inputs + +Salsa input quries work best if you can easily provide all of the inputs upfront. +However sometimes the set of inputs is not known beforehand. + +A typical example is reading files from disk. +While it is possible to eagarly scan a particular directory and create an in-memory file tree in a salsa input query, a more straight forward approach is to read the files lazily. +That is, when someone requests the text of a file for the first time: + +1. Read the file from disk and cache it. +2. Setup a file-system watcher for this path. +3. Innvalidate the cached file once the watcher sends a change notification. + +This is possible to achive in salsa, using a derived query and `report_synthetic_read` and `invalidate` queries. +The setup looks roughtly like this: + +```rust + +#[salsa::query_group(VfsDatabaseStorage)] +trait VfsDatabase: salsa::Database + FileWathcer { + fn read(&self, path: PathBuf) -> String; +} + +trait FileWatcher { + fn watch(&self, path: &Path); + fn did_change_file(&self, path: &Path); +} + +fn read(db: &impl salsa::Database, path: PathBuf) -> String { + db.salsa_runtime() + .report_synthetic_read(salsa::Durability::LOW); + db.watch(&path); + std::fs::read_to_string(&path).unwrap_or_default() +} + +#[salsa::database(VfsDatabaseStorage)] +struct MyDatabase { ... } + +impl FileWatcher for MyDatabase { + fn watch(&self, path: &Path) { ... } + fn did_change_file(&self, path: &Path) { + self.query_mut(ReadQuery).invalidate(path); + } +} +``` + +* We declare the query as a derived query (which is the default). +* In the query implementation, we don't call any other query and just directly read file from disk. +* Because the query doesn't read any inputs, it will be assigned a `HIGH` durability by default, which we override with `report_synthetic_read`. +* The result of the query is cached, and we must call `invalidate` to clear this cache. diff --git a/tests/on_demand_inputs.rs b/tests/on_demand_inputs.rs new file mode 100644 index 00000000..e7ba67e3 --- /dev/null +++ b/tests/on_demand_inputs.rs @@ -0,0 +1,110 @@ +//! Test that "on-demand" input pattern works. +//! +//! On-demand inputs are inputs computed lazily on the fly. They are simulated +//! via a b query with zero inputs, which uses `add_synthetic_read` to +//! tweak durability and `invalidate` to clear the input. + +use std::{cell::Cell, collections::HashMap, rc::Rc}; + +use salsa::{Database as _, Durability}; + +#[salsa::query_group(QueryGroupStorage)] +trait QueryGroup: salsa::Database + AsRef> { + fn a(&self, x: u32) -> u32; + fn b(&self, x: u32) -> u32; + fn c(&self, x: u32) -> u32; +} + +fn a(db: &impl QueryGroup, x: u32) -> u32 { + let durability = if x % 2 == 0 { + Durability::LOW + } else { + Durability::HIGH + }; + db.salsa_runtime().report_synthetic_read(durability); + let external_state: &HashMap = db.as_ref(); + external_state[&x] +} + +fn b(db: &impl QueryGroup, x: u32) -> u32 { + db.a(x) +} + +fn c(db: &impl QueryGroup, x: u32) -> u32 { + db.b(x) +} + +#[salsa::database(QueryGroupStorage)] +#[derive(Default)] +struct Database { + runtime: salsa::Runtime, + external_state: HashMap, + on_event: Option)>>, +} + +impl salsa::Database for Database { + fn salsa_runtime(&self) -> &salsa::Runtime { + &self.runtime + } + + fn salsa_event(&self, event_fn: impl Fn() -> salsa::Event) { + if let Some(cb) = &self.on_event { + cb(event_fn()) + } + } +} + +impl AsRef> for Database { + fn as_ref(&self) -> &HashMap { + &self.external_state + } +} + +#[test] +fn on_demand_input_works() { + let mut db = Database::default(); + + db.external_state.insert(1, 10); + assert_eq!(db.b(1), 10); + assert_eq!(db.a(1), 10); + + // We changed external state, but haven't signaled about this yet, + // so we expect to see the old answer + db.external_state.insert(1, 92); + assert_eq!(db.b(1), 10); + assert_eq!(db.a(1), 10); + + db.query_mut(AQuery).invalidate(&1); + assert_eq!(db.b(1), 92); + assert_eq!(db.a(1), 92); +} + +#[test] +fn on_demand_input_durability() { + let mut db = Database::default(); + db.external_state.insert(1, 10); + db.external_state.insert(2, 20); + assert_eq!(db.b(1), 10); + assert_eq!(db.b(2), 20); + + let validated = Rc::new(Cell::new(0)); + db.on_event = Some(Box::new({ + let validated = Rc::clone(&validated); + move |event| match event.kind { + salsa::EventKind::DidValidateMemoizedValue { .. } => validated.set(validated.get() + 1), + _ => (), + } + })); + + db.salsa_runtime().synthetic_write(Durability::LOW); + validated.set(0); + assert_eq!(db.c(1), 10); + assert_eq!(db.c(2), 20); + assert_eq!(validated.get(), 2); + + db.salsa_runtime().synthetic_write(Durability::HIGH); + validated.set(0); + assert_eq!(db.c(1), 10); + assert_eq!(db.c(2), 20); + assert_eq!(validated.get(), 4); +}