├── book ├── .gitignore ├── src │ ├── reference.md │ ├── plumbing │ │ ├── terminology.md │ │ ├── terminology │ │ │ ├── query.md │ │ │ ├── ingredient.md │ │ │ ├── salsa_item.md │ │ │ ├── durability.md │ │ │ ├── input_query.md │ │ │ ├── revision.md │ │ │ ├── salsa_struct.md │ │ │ ├── dependency.md │ │ │ ├── changed_at.md │ │ │ ├── backdate.md │ │ │ ├── derived_query.md │ │ │ ├── verified.md │ │ │ ├── LRU.md │ │ │ ├── query_function.md │ │ │ ├── untracked.md │ │ │ └── memo.md │ │ ├── query_ops.md │ │ ├── derived_flowchart.md │ │ ├── generated_code.md │ │ └── cycles.md │ ├── tutorial │ │ ├── checker.md │ │ ├── interpreter.md │ │ ├── db.md │ │ ├── accumulators.md │ │ └── debug.md │ ├── common_patterns.md │ ├── meta.md │ ├── reference │ │ └── durability.md │ ├── videos.md │ ├── about_salsa.md │ ├── tutorial.md │ ├── plumbing.md │ ├── tuning.md │ └── common_patterns │ │ └── on_demand_inputs.md ├── mermaid-init.js ├── book.toml └── netlify.sh ├── examples ├── lazy-input │ └── inputs │ │ ├── aa │ │ ├── b │ │ ├── a │ │ └── start └── calc │ ├── compile.rs │ ├── main.rs │ └── db.rs ├── components ├── salsa-macros │ ├── LICENSE-MIT │ ├── LICENSE-APACHE │ ├── Cargo.toml │ └── src │ │ ├── tracked.rs │ │ ├── debug.rs │ │ └── fn_util.rs └── salsa-macro-rules │ ├── LICENSE-MIT │ ├── LICENSE-APACHE │ ├── src │ ├── gate_accumulated.rs │ ├── macro_if.rs │ ├── maybe_default.rs │ ├── unexpected_cycle_recovery.rs │ ├── lib.rs │ ├── maybe_backdate.rs │ ├── setup_tracked_assoc_fn_body.rs │ └── setup_tracked_method_body.rs │ └── Cargo.toml ├── .dir-locals.el ├── .gitignore ├── tests ├── warnings │ ├── unused_variable_db.rs │ ├── main.rs │ ├── needless_borrow.rs │ └── needless_lifetimes.rs ├── compile-fail │ ├── invalid_update_field.rs │ ├── tracked_struct_not_update.rs │ ├── input_struct_unknown_attributes.rs │ ├── input_struct_unknown_attributes.stderr │ ├── lru_can_not_be_used_with_specify.stderr │ ├── span-input-setter.rs │ ├── interned_struct_unknown_attribute.rs │ ├── salsa_fields_incompatibles.rs │ ├── get-on-private-tracked-field.rs │ ├── get-on-private-interned-field.rs │ ├── lru_can_not_be_used_with_specify.rs │ ├── tracked_method_on_untracked_impl.rs │ ├── interned_not_update.rs │ ├── get-on-private-tracked-field.stderr │ ├── get-on-private-interned-field.stderr │ ├── derive_update_expansion_failure.rs │ ├── span-tracked-getter.rs │ ├── tracked_fn_return_not_update.rs │ ├── get-set-on-private-input-field.rs │ ├── incomplete_persistence.rs │ ├── tracked_struct_unknown_attribute.rs │ ├── derive_update_expansion_failure.stderr │ ├── salsa_fields_incompatibles.stderr │ ├── singleton_only_for_input.stderr │ ├── invalid_update_field.stderr │ ├── invalid_return_mode.rs │ ├── invalid_update_with.rs │ ├── specify-does-not-work-if-the-key-is-a-salsa-input.rs │ ├── specify-does-not-work-if-the-key-is-a-salsa-interned.rs │ ├── tracked_struct_unknown_attribute.stderr │ ├── interned_struct_unknown_attribute.stderr │ ├── interned_not_update.stderr │ ├── panic-when-reading-fields-of-tracked-structs-from-older-revisions.stderr │ ├── get-set-on-private-input-field.stderr │ ├── input_struct_incompatibles.rs │ ├── panic-when-reading-fields-of-tracked-structs-from-older-revisions.rs │ ├── singleton_only_for_input.rs │ ├── invalid_return_mode.stderr │ ├── span-input-setter.stderr │ ├── tracked_struct_incompatibles.rs │ ├── tracked_method_incompatibles.rs │ ├── accumulator_incompatibles.rs │ ├── tracked_method_on_untracked_impl.stderr │ ├── interned_struct_incompatibles.rs │ ├── span-tracked-getter.stderr │ ├── tracked_fn_return_ref.rs │ ├── invalid_persist_options.stderr │ ├── tracked_struct_incompatibles.stderr │ ├── specify-does-not-work-if-the-key-is-a-salsa-input.stderr │ ├── specify-does-not-work-if-the-key-is-a-salsa-interned.stderr │ ├── tracked_fn_return_ref.stderr │ ├── tracked_impl_incompatibles.rs │ ├── tracked_struct_not_update.stderr │ ├── invalid_persist_options.rs │ ├── interned_struct_incompatibles.stderr │ ├── invalid_update_with.stderr │ ├── input_struct_incompatibles.stderr │ ├── tracked_fn_incompatibles.rs │ ├── tracked_fn_return_not_update.stderr │ └── accumulator_incompatibles.stderr ├── compile_pass.rs ├── compile_fail.rs ├── tracked_fn_interned_lifetime.rs ├── compile-pass │ └── redefine-result-input-struct-derive.rs ├── tracked_struct_db1_lt.rs ├── tracked_with_intern.rs ├── panic-when-creating-tracked-struct-outside-of-tracked-fn.rs ├── tracked_fn_on_input.rs ├── tracked_fn_on_interned.rs ├── tracked_fn_on_tracked.rs ├── cycle_initial_call_query.rs ├── tracked_fn_constant.rs ├── cycle_result_dependencies.rs ├── tracked_fn_multiple_args.rs ├── tracked_fn_orphan_escape_hatch.rs ├── tracked_fn_return_ref.rs ├── hash_collision.rs ├── cycle_initial_call_back_into_cycle.rs ├── intern_access_in_different_revision.rs ├── tracked_method_inherent_return_ref.rs ├── tracked_method_inherent_return_deref.rs ├── cycle_recovery_call_query.rs ├── tracked_method_trait_return_ref.rs ├── tracked-struct-unchanged-in-new-rev.rs ├── mutate_in_place.rs ├── tracked_fn_high_durability_dependency.rs ├── tracked_struct_recreate_new_revision.rs ├── input_setter_preserves_durability.rs ├── parallel │ ├── main.rs │ ├── cycle_panic.rs │ └── signal.rs ├── tracked_with_struct_ord.rs ├── input_field_durability.rs ├── debug_bounds.rs ├── input_default.rs ├── tracked-struct-value-field-not-eq.rs ├── tracked-struct-id-field-bad-eq.rs ├── synthetic_write.rs ├── accumulate-custom-debug.rs ├── check_auto_traits.rs ├── cycle_regression_455.rs ├── tracked_method_with_self_ty.rs ├── tracked_fn_no_eq.rs ├── cycle_recovery_call_back_into_cycle.rs ├── singleton.rs ├── tracked_fn_on_tracked_specify.rs ├── specify-only-works-if-the-key-is-created-in-the-current-query.rs ├── tracked_fn_read_own_specify.rs ├── accumulate-chain.rs ├── durability.rs ├── cycle_fallback_immediate.rs ├── tracked_struct.rs ├── cycle_input_different_cycle_head.rs ├── tracked_method.rs ├── tracked_struct_with_interned_query.rs ├── accumulate-execution-order.rs ├── override_new_get_set.rs ├── tracked_method_on_tracked_struct.rs ├── tracked_with_struct_db.rs ├── derive_update.rs ├── tracked_struct_mixed_tracked_fields.rs ├── accumulate-dag.rs ├── elided-lifetime-in-tracked-fn.rs ├── debug_db_contents.rs ├── expect_reuse_field_x_of_an_input_changes_but_fn_depends_on_field_y.rs ├── tracked_fn_on_input_with_high_durability.rs ├── tracked_struct_manual_update.rs └── accumulate-from-tracked-fn.rs ├── release-plz.toml ├── justfile ├── .github ├── dependabot.yml └── workflows │ ├── release.yml │ └── book.yml ├── src ├── function │ ├── inputs.rs │ ├── lru.rs │ └── delete.rs ├── hash.rs ├── accumulator │ └── accumulated.rs ├── nonce.rs ├── database_impl.rs ├── input │ ├── singleton.rs │ └── setter.rs ├── cancelled.rs ├── tracing.rs └── return_mode.rs ├── RELEASES.md ├── LICENSE-MIT ├── .devcontainer └── devcontainer.json ├── FAQ.md └── benches └── incremental.rs /book/.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | -------------------------------------------------------------------------------- /book/src/reference.md: -------------------------------------------------------------------------------- 1 | # Reference 2 | -------------------------------------------------------------------------------- /examples/lazy-input/inputs/aa: -------------------------------------------------------------------------------- 1 | 8 2 | -------------------------------------------------------------------------------- /examples/lazy-input/inputs/b: -------------------------------------------------------------------------------- 1 | 4 2 | -------------------------------------------------------------------------------- /examples/lazy-input/inputs/a: -------------------------------------------------------------------------------- 1 | 2 2 | ./aa 3 | -------------------------------------------------------------------------------- /book/src/plumbing/terminology.md: -------------------------------------------------------------------------------- 1 | # Terminology 2 | -------------------------------------------------------------------------------- /book/src/plumbing/terminology/query.md: -------------------------------------------------------------------------------- 1 | # Query 2 | -------------------------------------------------------------------------------- /components/salsa-macros/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../../LICENSE-MIT -------------------------------------------------------------------------------- /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ((rust-mode (rust-format-on-save . t))) 2 | -------------------------------------------------------------------------------- /book/src/tutorial/checker.md: -------------------------------------------------------------------------------- 1 | # Defining the checker 2 | -------------------------------------------------------------------------------- /components/salsa-macro-rules/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../../LICENSE-MIT -------------------------------------------------------------------------------- /components/salsa-macros/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../../LICENSE-APACHE -------------------------------------------------------------------------------- /examples/lazy-input/inputs/start: -------------------------------------------------------------------------------- 1 | 1 2 | ./a 3 | ./b 4 | -------------------------------------------------------------------------------- /book/src/tutorial/interpreter.md: -------------------------------------------------------------------------------- 1 | # Defining the interpreter 2 | -------------------------------------------------------------------------------- /components/salsa-macro-rules/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../../LICENSE-APACHE -------------------------------------------------------------------------------- /book/mermaid-init.js: -------------------------------------------------------------------------------- 1 | mermaid.initialize({startOnLoad:true}); 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | TAGS 5 | nikom 6 | .idea 7 | -------------------------------------------------------------------------------- /tests/warnings/unused_variable_db.rs: -------------------------------------------------------------------------------- 1 | #[salsa::interned] 2 | struct Keywords<'db> {} 3 | -------------------------------------------------------------------------------- /book/src/common_patterns.md: -------------------------------------------------------------------------------- 1 | # Common patterns 2 | 3 | This section documents patterns for using Salsa. 4 | -------------------------------------------------------------------------------- /tests/compile-fail/invalid_update_field.rs: -------------------------------------------------------------------------------- 1 | #[derive(salsa::Update)] 2 | struct S2<'a> { 3 | bad2: &'a str, 4 | } 5 | 6 | fn main() {} 7 | -------------------------------------------------------------------------------- /book/src/plumbing/terminology/ingredient.md: -------------------------------------------------------------------------------- 1 | # Ingredient 2 | 3 | An *ingredient* is an individual piece of storage used to create a [salsa item](./salsa_item.md) -------------------------------------------------------------------------------- /book/src/plumbing/terminology/salsa_item.md: -------------------------------------------------------------------------------- 1 | # Salsa item 2 | 3 | A salsa item is something that is decorated with a `#[salsa::foo]` macro, like a tracked function or struct. -------------------------------------------------------------------------------- /tests/warnings/main.rs: -------------------------------------------------------------------------------- 1 | //! Test that macros don't generate code with warnings 2 | 3 | #![deny(warnings)] 4 | 5 | mod needless_borrow; 6 | mod needless_lifetimes; 7 | mod unused_variable_db; 8 | -------------------------------------------------------------------------------- /tests/compile-fail/tracked_struct_not_update.rs: -------------------------------------------------------------------------------- 1 | #[salsa::tracked] 2 | struct MyInput<'db> { 3 | field: NotUpdate, 4 | } 5 | 6 | #[derive(Clone, Debug, Hash)] 7 | struct NotUpdate; 8 | 9 | fn main() {} 10 | -------------------------------------------------------------------------------- /tests/warnings/needless_borrow.rs: -------------------------------------------------------------------------------- 1 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] 2 | enum Token {} 3 | 4 | #[salsa::tracked] 5 | struct TokenTree<'db> { 6 | #[returns(ref)] 7 | tokens: Vec, 8 | } 9 | -------------------------------------------------------------------------------- /tests/compile-fail/input_struct_unknown_attributes.rs: -------------------------------------------------------------------------------- 1 | #[salsa::input] 2 | struct InputWithUnknownAttrs { 3 | /// Doc comment 4 | field: u32, 5 | #[anything] 6 | field2: u32, 7 | } 8 | 9 | fn main() {} 10 | -------------------------------------------------------------------------------- /tests/compile-fail/input_struct_unknown_attributes.stderr: -------------------------------------------------------------------------------- 1 | error: cannot find attribute `anything` in this scope 2 | --> tests/compile-fail/input_struct_unknown_attributes.rs:5:7 3 | | 4 | 5 | #[anything] 5 | | ^^^^^^^^ 6 | -------------------------------------------------------------------------------- /book/src/plumbing/terminology/durability.md: -------------------------------------------------------------------------------- 1 | # Durability 2 | 3 | *Durability* is an optimization that we use to avoid checking the [dependencies] of a [query] individually. 4 | 5 | [dependencies]: ./dependency.md 6 | [query]: ./query.md 7 | -------------------------------------------------------------------------------- /release-plz.toml: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "salsa" 3 | version_group = "salsa" 4 | 5 | [[package]] 6 | name = "salsa-macros" 7 | version_group = "salsa" 8 | 9 | [[package]] 10 | name = "salsa-macro-rules" 11 | version_group = "salsa" 12 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | test: 2 | cargo test --workspace --all-targets --no-fail-fast 3 | 4 | miri: 5 | cargo +nightly miri test --no-fail-fast 6 | 7 | shuttle: 8 | cargo nextest run --features shuttle --test parallel 9 | 10 | all: test miri 11 | -------------------------------------------------------------------------------- /book/src/plumbing/terminology/input_query.md: -------------------------------------------------------------------------------- 1 | # Input query 2 | 3 | An *input query* is a [query] whose value is explicitly set by the user. When that value is set, a [durability] can also be provided. 4 | 5 | [query]: ./query.md 6 | [durability]: ./durability.md -------------------------------------------------------------------------------- /tests/compile_pass.rs: -------------------------------------------------------------------------------- 1 | #![cfg(all(feature = "inventory", feature = "persistence"))] 2 | 3 | #[rustversion::all(stable, since(1.90))] 4 | #[test] 5 | fn compile_pass() { 6 | let t = trybuild::TestCases::new(); 7 | t.pass("tests/compile-pass/*.rs"); 8 | } 9 | -------------------------------------------------------------------------------- /tests/compile_fail.rs: -------------------------------------------------------------------------------- 1 | #![cfg(all(feature = "inventory", feature = "persistence"))] 2 | 3 | #[rustversion::all(stable, since(1.90))] 4 | #[test] 5 | fn compile_fail() { 6 | let t = trybuild::TestCases::new(); 7 | t.compile_fail("tests/compile-fail/*.rs"); 8 | } 9 | -------------------------------------------------------------------------------- /book/src/plumbing/terminology/revision.md: -------------------------------------------------------------------------------- 1 | # Revision 2 | 3 | A *revision* is a monotonically increasing integer that we use to track the "version" of the database. Each time the value of an [input query] is modified, we create a new revision. 4 | 5 | [input query]: ./input_query.md -------------------------------------------------------------------------------- /book/src/plumbing/terminology/salsa_struct.md: -------------------------------------------------------------------------------- 1 | # Salsa struct 2 | 3 | A salsa struct is a struct decorated with one of the salsa macros: 4 | 5 | * `#[salsa::tracked]` 6 | * `#[salsa::input]` 7 | * `#[salsa::interned]` 8 | 9 | See the [salsa overview](../../overview.md) for more details. -------------------------------------------------------------------------------- /tests/compile-fail/lru_can_not_be_used_with_specify.stderr: -------------------------------------------------------------------------------- 1 | error: the `specify` and `lru` options cannot be used together 2 | --> tests/compile-fail/lru_can_not_be_used_with_specify.rs:6:27 3 | | 4 | 6 | #[salsa::tracked(lru = 3, specify)] 5 | | ^^^^^^^ 6 | -------------------------------------------------------------------------------- /book/src/plumbing/terminology/dependency.md: -------------------------------------------------------------------------------- 1 | # Dependency 2 | 3 | A *dependency* of a [query] Q is some other query Q1 that was invoked as part of computing the value for Q (typically, invoking by Q's [query function]). 4 | 5 | [query]: ./query.md 6 | [query function]: ./query_function.md -------------------------------------------------------------------------------- /tests/compile-fail/span-input-setter.rs: -------------------------------------------------------------------------------- 1 | #[salsa::input] 2 | pub struct MyInput { 3 | field: u32, 4 | } 5 | 6 | fn main() { 7 | let mut db = salsa::DatabaseImpl::new(); 8 | let input = MyInput::new(&mut db, 22); 9 | input.field(&db); 10 | input.set_field(22); 11 | } 12 | -------------------------------------------------------------------------------- /tests/compile-fail/interned_struct_unknown_attribute.rs: -------------------------------------------------------------------------------- 1 | #[salsa::interned] 2 | struct UnknownAttributeInterned { 3 | /// Test doc comment 4 | field: bool, 5 | #[unknown_attr] 6 | field2: bool, 7 | #[salsa::tracked] 8 | wrong_tracked: bool, 9 | } 10 | 11 | fn main() {} 12 | -------------------------------------------------------------------------------- /tests/compile-fail/salsa_fields_incompatibles.rs: -------------------------------------------------------------------------------- 1 | // Banned field name: `from` 2 | #[salsa::input] 3 | struct InputWithBannedName1 { 4 | from: u32, 5 | } 6 | 7 | // Banned field name: `new` 8 | #[salsa::input] 9 | struct InputWithBannedName2 { 10 | new: u32, 11 | } 12 | 13 | fn main() {} 14 | -------------------------------------------------------------------------------- /tests/compile-fail/get-on-private-tracked-field.rs: -------------------------------------------------------------------------------- 1 | mod a { 2 | #[salsa::tracked] 3 | pub struct MyTracked<'db> { 4 | field: u32, 5 | } 6 | } 7 | 8 | fn test<'db>(db: &'db dyn salsa::Database, tracked: a::MyTracked<'db>) { 9 | tracked.field(db); 10 | } 11 | 12 | fn main() {} 13 | -------------------------------------------------------------------------------- /tests/compile-fail/get-on-private-interned-field.rs: -------------------------------------------------------------------------------- 1 | mod a { 2 | #[salsa::interned] 3 | pub struct MyInterned<'db> { 4 | field: u32, 5 | } 6 | } 7 | 8 | fn test<'db>(db: &'db dyn salsa::Database, interned: a::MyInterned<'db>) { 9 | interned.field(db); 10 | } 11 | 12 | fn main() {} 13 | -------------------------------------------------------------------------------- /tests/compile-fail/lru_can_not_be_used_with_specify.rs: -------------------------------------------------------------------------------- 1 | #[salsa::input] 2 | struct MyInput { 3 | field: u32, 4 | } 5 | 6 | #[salsa::tracked(lru = 3, specify)] 7 | fn lru_can_not_be_used_with_specify(db: &dyn salsa::Database, input: MyInput) -> u32 { 8 | input.field(db) 9 | } 10 | 11 | fn main() {} 12 | -------------------------------------------------------------------------------- /tests/compile-fail/tracked_method_on_untracked_impl.rs: -------------------------------------------------------------------------------- 1 | #[salsa::input] 2 | struct MyInput { 3 | field: u32, 4 | } 5 | 6 | impl MyInput { 7 | #[salsa::tracked] 8 | fn tracked_method_on_untracked_impl(self, db: &dyn Db) -> u32 { 9 | input.field(db) 10 | } 11 | } 12 | 13 | fn main() {} 14 | -------------------------------------------------------------------------------- /tests/compile-fail/interned_not_update.rs: -------------------------------------------------------------------------------- 1 | use salsa::Database as Db; 2 | 3 | #[salsa::input] 4 | struct MyInput {} 5 | 6 | #[salsa::tracked] 7 | fn tracked_fn<'db>(_db: &'db dyn Db, _: (), _: &'db str) {} 8 | 9 | #[salsa::interned] 10 | struct Interned<'db> { 11 | _field: &'db str, 12 | } 13 | 14 | fn main() {} 15 | -------------------------------------------------------------------------------- /tests/compile-fail/get-on-private-tracked-field.stderr: -------------------------------------------------------------------------------- 1 | error[E0624]: method `field` is private 2 | --> tests/compile-fail/get-on-private-tracked-field.rs:9:13 3 | | 4 | 2 | #[salsa::tracked] 5 | | ----------------- private method defined here 6 | ... 7 | 9 | tracked.field(db); 8 | | ^^^^^ private method 9 | -------------------------------------------------------------------------------- /tests/compile-fail/get-on-private-interned-field.stderr: -------------------------------------------------------------------------------- 1 | error[E0624]: method `field` is private 2 | --> tests/compile-fail/get-on-private-interned-field.rs:9:14 3 | | 4 | 2 | #[salsa::interned] 5 | | ------------------ private method defined here 6 | ... 7 | 9 | interned.field(db); 8 | | ^^^^^ private method 9 | -------------------------------------------------------------------------------- /tests/compile-fail/derive_update_expansion_failure.rs: -------------------------------------------------------------------------------- 1 | #[derive(salsa::Update)] 2 | union U { 3 | field: i32, 4 | } 5 | 6 | #[derive(salsa::Update)] 7 | struct S { 8 | #[update(with(missing_unsafe))] 9 | bad: i32, 10 | } 11 | 12 | fn missing_unsafe(_: *mut i32, _: i32) -> bool { 13 | true 14 | } 15 | 16 | fn main() {} 17 | -------------------------------------------------------------------------------- /components/salsa-macro-rules/src/gate_accumulated.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "accumulator")] 2 | #[macro_export] 3 | macro_rules! gate_accumulated { 4 | ($($body:tt)*) => { 5 | $($body)* 6 | }; 7 | } 8 | 9 | #[cfg(not(feature = "accumulator"))] 10 | #[macro_export] 11 | macro_rules! gate_accumulated { 12 | ($($body:tt)*) => {}; 13 | } 14 | -------------------------------------------------------------------------------- /examples/calc/compile.rs: -------------------------------------------------------------------------------- 1 | use crate::ir::SourceProgram; 2 | use crate::parser::parse_statements; 3 | use crate::type_check::type_check_program; 4 | 5 | #[salsa::tracked] 6 | pub fn compile(db: &dyn crate::Db, source_program: SourceProgram) { 7 | let program = parse_statements(db, source_program); 8 | type_check_program(db, program); 9 | } 10 | -------------------------------------------------------------------------------- /tests/compile-fail/span-tracked-getter.rs: -------------------------------------------------------------------------------- 1 | #[salsa::tracked] 2 | pub struct MyTracked<'db> { 3 | field: u32, 4 | } 5 | 6 | #[salsa::tracked] 7 | fn my_fn(db: &dyn salsa::Database) { 8 | let x = MyTracked::new(db, 22); 9 | x.field(22); 10 | } 11 | 12 | fn main() { 13 | let mut db = salsa::DatabaseImpl::new(); 14 | my_fn(&db); 15 | } 16 | -------------------------------------------------------------------------------- /tests/compile-fail/tracked_fn_return_not_update.rs: -------------------------------------------------------------------------------- 1 | use salsa::Database as Db; 2 | 3 | #[salsa::input] 4 | struct MyInput {} 5 | 6 | #[derive(Clone, Debug)] 7 | struct NotUpdate; 8 | 9 | #[salsa::tracked] 10 | fn tracked_fn<'db>(db: &'db dyn Db, input: MyInput) -> NotUpdate { 11 | _ = (db, input); 12 | NotUpdate 13 | } 14 | 15 | fn main() {} 16 | -------------------------------------------------------------------------------- /tests/compile-fail/get-set-on-private-input-field.rs: -------------------------------------------------------------------------------- 1 | mod a { 2 | #[salsa::input] 3 | pub struct MyInput { 4 | field: u32, 5 | } 6 | } 7 | 8 | fn main() { 9 | let mut db = salsa::DatabaseImpl::new(); 10 | let input = a::MyInput::new(&mut db, 22); 11 | 12 | input.field(&db); 13 | input.set_field(&mut db).to(23); 14 | } 15 | -------------------------------------------------------------------------------- /tests/compile-fail/incomplete_persistence.rs: -------------------------------------------------------------------------------- 1 | #[salsa::tracked(persist)] 2 | struct Persistable<'db> { 3 | field: NotPersistable<'db>, 4 | } 5 | 6 | #[salsa::tracked] 7 | struct NotPersistable<'db> { 8 | field: usize, 9 | } 10 | 11 | #[salsa::tracked(persist)] 12 | fn query(_db: &dyn salsa::Database, _input: NotPersistable<'_>) {} 13 | 14 | fn main() {} 15 | -------------------------------------------------------------------------------- /tests/compile-fail/tracked_struct_unknown_attribute.rs: -------------------------------------------------------------------------------- 1 | #[salsa::tracked] 2 | struct UnknownAttributeTrackedStruct<'db> { 3 | #[tracked] 4 | tracked: bool, 5 | #[unknown_attr] 6 | field: bool, 7 | #[salsa::tracked] 8 | wrong_tracked: bool, 9 | /// TestDocComment 10 | /// TestDocComment 11 | field_with_doc: bool 12 | } 13 | 14 | fn main() {} 15 | -------------------------------------------------------------------------------- /components/salsa-macro-rules/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "salsa-macro-rules" 3 | version = "0.25.2" 4 | authors.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | rust-version.workspace = true 9 | description = "Declarative macros for the salsa crate" 10 | 11 | [dependencies] 12 | 13 | [features] 14 | accumulator = [] 15 | -------------------------------------------------------------------------------- /tests/compile-fail/derive_update_expansion_failure.stderr: -------------------------------------------------------------------------------- 1 | error: `derive(Update)` does not support `union` 2 | --> tests/compile-fail/derive_update_expansion_failure.rs:2:1 3 | | 4 | 2 | union U { 5 | | ^^^^^ 6 | 7 | error: expected `unsafe` 8 | --> tests/compile-fail/derive_update_expansion_failure.rs:8:14 9 | | 10 | 8 | #[update(with(missing_unsafe))] 11 | | ^^^^ 12 | -------------------------------------------------------------------------------- /book/src/plumbing/query_ops.md: -------------------------------------------------------------------------------- 1 | # Query operations 2 | 3 | The most important basic operations that all queries support are: 4 | 5 | * [maybe changed after](./maybe_changed_after.md): Returns true if the value of the query (for the given key) may have changed since the given revision. 6 | * [Fetch](./fetch.md): Returns the up-to-date value for the given K (or an error in the case of an "unrecovered" cycle). 7 | -------------------------------------------------------------------------------- /tests/compile-fail/salsa_fields_incompatibles.stderr: -------------------------------------------------------------------------------- 1 | error: the field name `from` is disallowed in salsa structs 2 | --> tests/compile-fail/salsa_fields_incompatibles.rs:4:5 3 | | 4 | 4 | from: u32, 5 | | ^^^^ 6 | 7 | error: the field name `new` is disallowed in salsa structs 8 | --> tests/compile-fail/salsa_fields_incompatibles.rs:10:5 9 | | 10 | 10 | new: u32, 11 | | ^^^ 12 | -------------------------------------------------------------------------------- /tests/tracked_fn_interned_lifetime.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | #[salsa::interned] 4 | struct Interned<'db> { 5 | field: i32, 6 | } 7 | 8 | #[salsa::tracked] 9 | fn foo<'a>(_db: &'a dyn salsa::Database, _: Interned<'_>, _: Interned<'a>) {} 10 | 11 | #[test] 12 | fn the_test() { 13 | let db = salsa::DatabaseImpl::new(); 14 | let i = Interned::new(&db, 123); 15 | foo(&db, i, i); 16 | } 17 | -------------------------------------------------------------------------------- /book/src/plumbing/terminology/changed_at.md: -------------------------------------------------------------------------------- 1 | # Changed at 2 | 3 | The *changed at* revision for a [memo] is the [revision] in which that memo's value last changed. Typically, this is the same as the revision in which the [query function] was last executed, but it may be an earlier revision if the memo was [backdated]. 4 | 5 | [query function]: ./query_function.md 6 | [backdated]: ./backdate.md 7 | [revision]: ./revision.md 8 | [memo]: ./memo.md -------------------------------------------------------------------------------- /tests/compile-fail/singleton_only_for_input.stderr: -------------------------------------------------------------------------------- 1 | error: `singleton` option not allowed here 2 | --> tests/compile-fail/singleton_only_for_input.rs:15:18 3 | | 4 | 15 | #[salsa::tracked(singleton)] 5 | | ^^^^^^^^^ 6 | 7 | error: `singleton` option not allowed here 8 | --> tests/compile-fail/singleton_only_for_input.rs:22:22 9 | | 10 | 22 | #[salsa::accumulator(singleton)] 11 | | ^^^^^^^^^ 12 | -------------------------------------------------------------------------------- /book/src/plumbing/terminology/backdate.md: -------------------------------------------------------------------------------- 1 | # Backdate 2 | 3 | *Backdating* is when we mark a value that was computed in revision R as having last changed in some earlier revision. This is done when we have an older [memo] M and we can compare the two values to see that, while the [dependencies] to M may have changed, the result of the [query function] did not. 4 | 5 | [memo]: ./memo.md 6 | [dependencies]: ./dependency.md 7 | [query function]: ./query_function.md -------------------------------------------------------------------------------- /book/src/plumbing/terminology/derived_query.md: -------------------------------------------------------------------------------- 1 | # Derived query 2 | 3 | A *derived query* is a [query] whose value is defined by the result of a user-provided [query function]. That function is executed to get the result of the query. Unlike [input queries], the result of a derived queries can always be recomputed whenever needed simply by re-executing the function. 4 | 5 | [query]: ./query.md 6 | [query function]: ./query_function.md 7 | [input queries]: ./input_query.md -------------------------------------------------------------------------------- /tests/compile-fail/invalid_update_field.stderr: -------------------------------------------------------------------------------- 1 | error: lifetime may not live long enough 2 | --> tests/compile-fail/invalid_update_field.rs:1:10 3 | | 4 | 1 | #[derive(salsa::Update)] 5 | | ^^^^^^^^^^^^^ requires that `'a` must outlive `'static` 6 | 2 | struct S2<'a> { 7 | | -- lifetime `'a` defined here 8 | | 9 | = note: this error originates in the derive macro `salsa::Update` (in Nightly builds, run with -Z macro-backtrace for more info) 10 | -------------------------------------------------------------------------------- /tests/compile-fail/invalid_return_mode.rs: -------------------------------------------------------------------------------- 1 | use salsa::Database as Db; 2 | 3 | #[salsa::input] 4 | struct MyInput { 5 | #[returns(clone)] 6 | text: String, 7 | } 8 | 9 | #[salsa::tracked(returns(not_a_return_mode))] 10 | fn tracked_fn_invalid_return_mode(db: &dyn Db, input: MyInput) -> String { 11 | input.text(db) 12 | } 13 | 14 | #[salsa::input] 15 | struct MyInvalidInput { 16 | #[returns(not_a_return_mode)] 17 | text: String, 18 | } 19 | 20 | fn main() { } -------------------------------------------------------------------------------- /tests/compile-pass/redefine-result-input-struct-derive.rs: -------------------------------------------------------------------------------- 1 | // Ensure the `salsa::tracked` attribute macro doesn't conflict with local 2 | // redefinition of the `Result` type. 3 | // 4 | // See: https://github.com/salsa-rs/salsa/pull/1025 5 | 6 | type Result = std::result::Result; 7 | 8 | #[salsa::tracked] 9 | fn example_query(_db: &dyn salsa::Database) -> Result<()> { 10 | Ok(()) 11 | } 12 | 13 | fn main() { 14 | println!("Hello, world!"); 15 | } 16 | -------------------------------------------------------------------------------- /tests/tracked_struct_db1_lt.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! Test that tracked structs with lifetimes not named `'db` 4 | //! compile successfully. 5 | 6 | mod common; 7 | 8 | use test_log::test; 9 | 10 | #[salsa::input] 11 | struct MyInput { 12 | field: u32, 13 | } 14 | 15 | #[salsa::tracked] 16 | struct MyTracked1<'db1> { 17 | field: MyTracked2<'db1>, 18 | } 19 | 20 | #[salsa::tracked] 21 | struct MyTracked2<'db2> { 22 | field: u32, 23 | } 24 | 25 | #[test] 26 | fn create_db() {} 27 | -------------------------------------------------------------------------------- /tests/tracked_with_intern.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! Test that a setting a field on a `#[salsa::input]` 4 | //! overwrites and returns the old value. 5 | 6 | use test_log::test; 7 | 8 | #[salsa::input] 9 | struct MyInput { 10 | field: String, 11 | } 12 | 13 | #[salsa::tracked] 14 | struct MyTracked<'db> { 15 | #[tracked] 16 | field: MyInterned<'db>, 17 | } 18 | 19 | #[salsa::interned] 20 | struct MyInterned<'db> { 21 | field: String, 22 | } 23 | 24 | #[test] 25 | fn execute() {} 26 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for more information: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | # https://containers.dev/guide/dependabot 6 | 7 | version: 2 8 | updates: 9 | - package-ecosystem: "devcontainers" 10 | directory: "/" 11 | schedule: 12 | interval: weekly 13 | -------------------------------------------------------------------------------- /examples/calc/main.rs: -------------------------------------------------------------------------------- 1 | use db::CalcDatabaseImpl; 2 | use ir::{Diagnostic, SourceProgram}; 3 | use salsa::Database as Db; 4 | 5 | mod compile; 6 | mod db; 7 | mod ir; 8 | mod parser; 9 | mod type_check; 10 | 11 | pub fn main() { 12 | let db: CalcDatabaseImpl = Default::default(); 13 | let source_program = SourceProgram::new(&db, String::new()); 14 | compile::compile(&db, source_program); 15 | let diagnostics = compile::compile::accumulated::(&db, source_program); 16 | eprintln!("{diagnostics:?}"); 17 | } 18 | -------------------------------------------------------------------------------- /tests/compile-fail/invalid_update_with.rs: -------------------------------------------------------------------------------- 1 | #[derive(salsa::Update)] 2 | struct S2 { 3 | #[update(unsafe(with(my_wrong_update)))] 4 | bad: i32, 5 | #[update(unsafe(with(my_wrong_update2)))] 6 | bad2: i32, 7 | #[update(unsafe(with(my_wrong_update3)))] 8 | bad3: i32, 9 | #[update(unsafe(with(true)))] 10 | bad4: &'static str, 11 | } 12 | 13 | fn my_wrong_update() {} 14 | fn my_wrong_update2(_: (), _: ()) -> bool { 15 | true 16 | } 17 | fn my_wrong_update3(_: *mut i32, _: i32) -> () {} 18 | 19 | fn main() {} 20 | -------------------------------------------------------------------------------- /tests/panic-when-creating-tracked-struct-outside-of-tracked-fn.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! Test that creating a tracked struct outside of a 4 | //! tracked function panics with an assert message. 5 | 6 | #[salsa::tracked] 7 | struct MyTracked<'db> { 8 | field: u32, 9 | } 10 | 11 | #[test] 12 | #[should_panic( 13 | expected = "cannot create a tracked struct disambiguator outside of a tracked function" 14 | )] 15 | fn execute() { 16 | let db = salsa::DatabaseImpl::new(); 17 | MyTracked::new(&db, 0); 18 | } 19 | -------------------------------------------------------------------------------- /tests/compile-fail/specify-does-not-work-if-the-key-is-a-salsa-input.rs: -------------------------------------------------------------------------------- 1 | //! Test that `specify` does not work if the key is a `salsa::input` 2 | //! compilation fails 3 | #![allow(warnings)] 4 | 5 | #[salsa::input] 6 | struct MyInput { 7 | field: u32, 8 | } 9 | 10 | #[salsa::tracked] 11 | struct MyTracked<'db> { 12 | field: u32, 13 | } 14 | 15 | #[salsa::tracked(specify)] 16 | fn tracked_fn<'db>(db: &'db dyn salsa::Database, input: MyInput) -> MyTracked<'db> { 17 | MyTracked::new(db, input.field(db) * 2) 18 | } 19 | 20 | fn main() {} 21 | -------------------------------------------------------------------------------- /book/src/plumbing/terminology/verified.md: -------------------------------------------------------------------------------- 1 | # Verified 2 | 3 | A [memo] is *verified* in a revision R if we have checked that its value is still up-to-date (i.e., if we were to reexecute the [query function], we are guaranteed to get the same result). Each memo tracks the revision in which it was last verified to avoid repeatedly checking whether dependencies have changed during the [fetch] and [maybe changed after] operations. 4 | 5 | [query function]: ./query_function.md 6 | [fetch]: ../fetch.md 7 | [maybe changed after]: ../maybe_changed_after.md 8 | [memo]: ./memo.md -------------------------------------------------------------------------------- /book/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Salsa Contributors"] 3 | multilingual = false 4 | src = "src" 5 | title = "Salsa" 6 | 7 | [build] 8 | create-missing = false 9 | 10 | [preprocess.links] 11 | 12 | [output.html] 13 | additional-css =["mermaid.css"] 14 | additional-js =["mermaid.min.js", "mermaid-init.js"] 15 | 16 | [output.linkcheck] 17 | # follow-web-links = true --- this is commented out b/c of false errors 18 | traverse-parent-directories = false 19 | exclude = ['bilibili\.com'] 20 | [preprocessor] 21 | [preprocessor.mermaid] 22 | command = "mdbook-mermaid" 23 | -------------------------------------------------------------------------------- /components/salsa-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "salsa-macros" 3 | version = "0.25.2" 4 | authors.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | rust-version.workspace = true 9 | description = "Procedural macros for the salsa crate" 10 | 11 | [lib] 12 | proc-macro = true 13 | 14 | [dependencies] 15 | proc-macro2 = "1.0" 16 | quote = "1.0" 17 | syn = { version = "2.0.104", features = ["full", "visit-mut"] } 18 | synstructure = "0.13.2" 19 | 20 | [features] 21 | default = [] 22 | persistence = [] 23 | -------------------------------------------------------------------------------- /src/function/inputs.rs: -------------------------------------------------------------------------------- 1 | use crate::function::{Configuration, IngredientImpl}; 2 | use crate::zalsa::Zalsa; 3 | use crate::zalsa_local::QueryOriginRef; 4 | use crate::Id; 5 | 6 | impl IngredientImpl 7 | where 8 | C: Configuration, 9 | { 10 | pub(super) fn origin<'db>(&self, zalsa: &'db Zalsa, key: Id) -> Option> { 11 | let memo_ingredient_index = self.memo_ingredient_index(zalsa, key); 12 | self.get_memo_from_table_for(zalsa, key, memo_ingredient_index) 13 | .map(|m| m.revisions.origin.as_ref()) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/compile-fail/specify-does-not-work-if-the-key-is-a-salsa-interned.rs: -------------------------------------------------------------------------------- 1 | //! Test that `specify` does not work if the key is a `salsa::interned` 2 | //! compilation fails 3 | #![allow(warnings)] 4 | 5 | #[salsa::interned] 6 | struct MyInterned<'db> { 7 | field: u32, 8 | } 9 | 10 | #[salsa::tracked] 11 | struct MyTracked<'db> { 12 | field: u32, 13 | } 14 | 15 | #[salsa::tracked(specify)] 16 | fn tracked_fn<'db>(db: &'db dyn salsa::Database, input: MyInterned<'db>) -> MyTracked<'db> { 17 | MyTracked::new(db, input.field(db) * 2) 18 | } 19 | 20 | fn main() {} 21 | -------------------------------------------------------------------------------- /tests/tracked_fn_on_input.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! Test that a `tracked` fn on a `salsa::input` 4 | //! compiles and executes successfully. 5 | #![allow(warnings)] 6 | 7 | #[salsa::input] 8 | struct MyInput { 9 | field: u32, 10 | } 11 | 12 | #[salsa::tracked] 13 | fn tracked_fn(db: &dyn salsa::Database, input: MyInput) -> u32 { 14 | input.field(db) * 2 15 | } 16 | 17 | #[test] 18 | fn execute() { 19 | let mut db = salsa::DatabaseImpl::new(); 20 | let input = MyInput::new(&db, 22); 21 | assert_eq!(tracked_fn(&db, input), 44); 22 | } 23 | -------------------------------------------------------------------------------- /tests/compile-fail/tracked_struct_unknown_attribute.stderr: -------------------------------------------------------------------------------- 1 | error: only a single lifetime parameter is accepted 2 | --> tests/compile-fail/tracked_struct_unknown_attribute.rs:1:1 3 | | 4 | 1 | #[salsa::tracked] 5 | | ^^^^^^^^^^^^^^^^^ 6 | | 7 | = note: this error originates in the attribute macro `salsa::tracked` (in Nightly builds, run with -Z macro-backtrace for more info) 8 | 9 | error: cannot find attribute `unknown_attr` in this scope 10 | --> tests/compile-fail/tracked_struct_unknown_attribute.rs:5:7 11 | | 12 | 5 | #[unknown_attr] 13 | | ^^^^^^^^^^^^ 14 | -------------------------------------------------------------------------------- /tests/compile-fail/interned_struct_unknown_attribute.stderr: -------------------------------------------------------------------------------- 1 | error: only a single lifetime parameter is accepted 2 | --> tests/compile-fail/interned_struct_unknown_attribute.rs:1:1 3 | | 4 | 1 | #[salsa::interned] 5 | | ^^^^^^^^^^^^^^^^^^ 6 | | 7 | = note: this error originates in the attribute macro `salsa::interned` (in Nightly builds, run with -Z macro-backtrace for more info) 8 | 9 | error: cannot find attribute `unknown_attr` in this scope 10 | --> tests/compile-fail/interned_struct_unknown_attribute.rs:5:7 11 | | 12 | 5 | #[unknown_attr] 13 | | ^^^^^^^^^^^^ 14 | -------------------------------------------------------------------------------- /book/src/plumbing/derived_flowchart.md: -------------------------------------------------------------------------------- 1 | # Derived queries flowchart 2 | 3 | Derived queries are by far the most complex. This flowchart documents the flow of the [maybe changed after] and [fetch] operations. This flowchart can be edited on [draw.io]: 4 | 5 | [draw.io]: https://draw.io 6 | [fetch]: ./fetch.md 7 | [maybe changed after]: ./maybe_changed_after.md 8 | 9 | 10 |
11 | 12 | ![Flowchart](../derived-query-read.drawio.svg) 13 | 14 |
15 | -------------------------------------------------------------------------------- /book/src/plumbing/terminology/LRU.md: -------------------------------------------------------------------------------- 1 | # LRU 2 | 3 | The [`set_lru_capacity`](https://docs.rs/salsa/0.16.1/salsa/struct.QueryTableMut.html#method.set_lru_capacity) method can be used to fix the maximum capacity for a query at a specific number of values. If more values are added after that point, then salsa will drop the values from older [memos] to conserve memory (we always retain the [dependency] information for those memos, however, so that we can still compute whether values may have changed, even if we don't know what that value is). 4 | 5 | [memos]: ./memo.md 6 | [dependency]: ./dependency.md 7 | -------------------------------------------------------------------------------- /tests/tracked_fn_on_interned.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! Test that a `tracked` fn on a `salsa::interned` 4 | //! compiles and executes successfully. 5 | 6 | #[salsa::interned] 7 | struct Name<'db> { 8 | name: String, 9 | } 10 | 11 | #[salsa::tracked] 12 | fn tracked_fn<'db>(db: &'db dyn salsa::Database, name: Name<'db>) -> String { 13 | name.name(db).clone() 14 | } 15 | 16 | #[test] 17 | fn execute() { 18 | let db = salsa::DatabaseImpl::new(); 19 | let name = Name::new(&db, "Salsa".to_string()); 20 | 21 | assert_eq!(tracked_fn(&db, name), "Salsa"); 22 | } 23 | -------------------------------------------------------------------------------- /tests/compile-fail/interned_not_update.stderr: -------------------------------------------------------------------------------- 1 | error: lifetime may not live long enough 2 | --> tests/compile-fail/interned_not_update.rs:7:48 3 | | 4 | 7 | fn tracked_fn<'db>(_db: &'db dyn Db, _: (), _: &'db str) {} 5 | | --- lifetime `'db` defined here ^ requires that `'db` must outlive `'static` 6 | 7 | error: lifetime may not live long enough 8 | --> tests/compile-fail/interned_not_update.rs:11:13 9 | | 10 | 10 | struct Interned<'db> { 11 | | --- lifetime `'db` defined here 12 | 11 | _field: &'db str, 13 | | ^ requires that `'db` must outlive `'static` 14 | -------------------------------------------------------------------------------- /tests/compile-fail/panic-when-reading-fields-of-tracked-structs-from-older-revisions.stderr: -------------------------------------------------------------------------------- 1 | error[E0502]: cannot borrow `db` as mutable because it is also borrowed as immutable 2 | --> tests/compile-fail/panic-when-reading-fields-of-tracked-structs-from-older-revisions.rs:22:21 3 | | 4 | 21 | let tracked = tracked_fn(&db, input); 5 | | --- immutable borrow occurs here 6 | 22 | input.set_field(&mut db).to(24); 7 | | ^^^^^^^ mutable borrow occurs here 8 | 23 | tracked.field(&db); // tracked comes from prior revision 9 | | ------- immutable borrow later used here 10 | -------------------------------------------------------------------------------- /book/src/plumbing/terminology/query_function.md: -------------------------------------------------------------------------------- 1 | # Query function 2 | 3 | The *query function* is the user-provided function that we execute to compute the value of a [derived query]. Salsa assumed that all query functions are a 'pure' function of their [dependencies] unless the user reports an [untracked read]. Salsa always assumes that functions have no important side-effects (i.e., that they don't send messages over the network whose results you wish to observe) and thus that it doesn't have to re-execute functions unless it needs their return value. 4 | 5 | [derived query]: ./derived_query.md 6 | [dependencies]: ./dependency.md 7 | [untracked read]: ./untracked.md -------------------------------------------------------------------------------- /tests/compile-fail/get-set-on-private-input-field.stderr: -------------------------------------------------------------------------------- 1 | error[E0624]: method `field` is private 2 | --> tests/compile-fail/get-set-on-private-input-field.rs:12:11 3 | | 4 | 2 | #[salsa::input] 5 | | --------------- private method defined here 6 | ... 7 | 12 | input.field(&db); 8 | | ^^^^^ private method 9 | 10 | error[E0624]: method `set_field` is private 11 | --> tests/compile-fail/get-set-on-private-input-field.rs:13:11 12 | | 13 | 2 | #[salsa::input] 14 | | --------------- private method defined here 15 | ... 16 | 13 | input.set_field(&mut db).to(23); 17 | | ^^^^^^^^^ private method 18 | -------------------------------------------------------------------------------- /tests/compile-fail/input_struct_incompatibles.rs: -------------------------------------------------------------------------------- 1 | #[salsa::input(returns(ref))] 2 | struct InputWithRetRef(u32); 3 | 4 | #[salsa::input(specify)] 5 | struct InputWithSpecify(u32); 6 | 7 | #[salsa::input(no_eq)] 8 | struct InputNoWithEq(u32); 9 | 10 | #[salsa::input(db = Db)] 11 | struct InputWithDb(u32); 12 | 13 | #[salsa::input(recover_fn = recover)] 14 | struct InputWithRecover(u32); 15 | 16 | #[salsa::input(lru =12)] 17 | struct InputWithLru(u32); 18 | 19 | #[salsa::input(revisions = 12)] 20 | struct InputWithRevisions(u32); 21 | 22 | #[salsa::input] 23 | struct InputWithTrackedField { 24 | #[tracked] 25 | field: u32, 26 | } 27 | 28 | fn main() {} 29 | -------------------------------------------------------------------------------- /tests/warnings/needless_lifetimes.rs: -------------------------------------------------------------------------------- 1 | #[salsa::db] 2 | pub trait Db: salsa::Database {} 3 | 4 | #[derive(Debug, PartialEq, Eq, Hash)] 5 | pub struct Item {} 6 | 7 | #[salsa::tracked] 8 | pub struct SourceTree<'db> {} 9 | 10 | #[salsa::tracked] 11 | impl<'db> SourceTree<'db> { 12 | #[salsa::tracked(returns(ref))] 13 | pub fn all_items(self, _db: &'db dyn Db) -> Vec { 14 | todo!() 15 | } 16 | } 17 | 18 | #[salsa::tracked(returns(ref))] 19 | fn use_tree<'db>(_db: &'db dyn Db, _tree: SourceTree<'db>) {} 20 | 21 | #[allow(unused)] 22 | fn use_it(db: &dyn Db, tree: SourceTree) { 23 | tree.all_items(db); 24 | use_tree(db, tree); 25 | } 26 | -------------------------------------------------------------------------------- /tests/tracked_fn_on_tracked.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! Test that a `tracked` fn on a `salsa::input` 4 | //! compiles and executes successfully. 5 | 6 | #[salsa::input] 7 | struct MyInput { 8 | field: u32, 9 | } 10 | 11 | #[salsa::tracked] 12 | struct MyTracked<'db> { 13 | field: u32, 14 | } 15 | 16 | #[salsa::tracked] 17 | fn tracked_fn(db: &dyn salsa::Database, input: MyInput) -> MyTracked<'_> { 18 | MyTracked::new(db, input.field(db) * 2) 19 | } 20 | 21 | #[test] 22 | fn execute() { 23 | let db = salsa::DatabaseImpl::new(); 24 | let input = MyInput::new(&db, 22); 25 | assert_eq!(tracked_fn(&db, input).field(&db), 44); 26 | } 27 | -------------------------------------------------------------------------------- /book/src/meta.md: -------------------------------------------------------------------------------- 1 | # Meta: about the book itself 2 | 3 | ## Linking policy 4 | 5 | We try to avoid links that easily become fragile. 6 | 7 | **Do:** 8 | 9 | * Link to `docs.rs` types to document the public API, but modify the link to use `latest` as the version. 10 | * Link to modules in the source code. 11 | * Create ["named anchors"] and embed source code directly. 12 | 13 | ["named anchors"]: https://rust-lang.github.io/mdBook/format/mdbook.html?highlight=ANCHOR#including-portions-of-a-file 14 | 15 | **Don't:** 16 | 17 | * Link to direct lines on github, even within a specific commit, unless you are trying to reference a historical piece of code ("how things were at the time"). -------------------------------------------------------------------------------- /RELEASES.md: -------------------------------------------------------------------------------- 1 | # 0.13.0 2 | 3 | - **Breaking change:** adopt the new `Durability` API proposed in [RFC #6] 4 | - this replaces and generalizes the existing concepts of constants 5 | - **Breaking change:** remove "volatile" queries 6 | - instead, create a normal query which invokes the 7 | `report_untracked_read` method on the salsa runtime 8 | - introduce "slots", an optimization to salsa's internal workings 9 | - document `#[salsa::requires]` attribute, which permits private dependencies 10 | - Adopt `AtomicU64` for `runtimeId` (#182) 11 | - use `ptr::eq` and `ptr::hash` for readability 12 | - upgrade parking lot, rand dependencies 13 | 14 | [RFC #6]: https://github.com/salsa-rs/salsa-rfcs/pull/6 15 | -------------------------------------------------------------------------------- /tests/compile-fail/panic-when-reading-fields-of-tracked-structs-from-older-revisions.rs: -------------------------------------------------------------------------------- 1 | use salsa::prelude::*; 2 | 3 | #[salsa::input] 4 | struct MyInput { 5 | field: u32, 6 | } 7 | 8 | #[salsa::tracked] 9 | struct MyTracked<'db> { 10 | field: u32, 11 | } 12 | 13 | #[salsa::tracked] 14 | fn tracked_fn<'db>(db: &'db dyn salsa::Database, input: MyInput) -> MyTracked<'db> { 15 | MyTracked::new(db, input.field(db) / 2) 16 | } 17 | 18 | fn main() { 19 | let mut db = salsa::DatabaseImpl::new(); 20 | let input = MyInput::new(&db, 22); 21 | let tracked = tracked_fn(&db, input); 22 | input.set_field(&mut db).to(24); 23 | tracked.field(&db); // tracked comes from prior revision 24 | } 25 | -------------------------------------------------------------------------------- /tests/compile-fail/singleton_only_for_input.rs: -------------------------------------------------------------------------------- 1 | //! Compile Singleton struct test: 2 | //! 3 | //! Singleton flags are only allowed for input structs. If applied on any other Salsa struct compilation must fail 4 | 5 | #[salsa::input(singleton)] 6 | struct MyInput { 7 | field: u32, 8 | } 9 | 10 | #[salsa::tracked(singleton)] 11 | struct MyTracked<'db> { 12 | field: u32, 13 | } 14 | 15 | #[salsa::tracked(singleton)] 16 | fn create_tracked_structs(db: &dyn salsa::Database, input: MyInput) -> Vec { 17 | (0..input.field(db)) 18 | .map(|i| MyTracked::new(db, i)) 19 | .collect() 20 | } 21 | 22 | #[salsa::accumulator(singleton)] 23 | struct Integers(u32); 24 | 25 | fn main() {} 26 | -------------------------------------------------------------------------------- /book/src/reference/durability.md: -------------------------------------------------------------------------------- 1 | # Durability 2 | 3 | "Durability" is an optimization that can greatly improve the performance of your salsa programs. 4 | Durability specifies the probability that an input's value will change. 5 | The default is "low durability". 6 | But when you set the value of an input, you can manually specify a higher durability, 7 | typically `Durability::HIGH`. 8 | Salsa tracks when tracked functions only consume values of high durability 9 | and, if no high durability input has changed, it can skip traversing their 10 | dependencies. 11 | 12 | Typically "high durability" values are things like data read from the standard library 13 | or other inputs that aren't actively being edited by the end user. -------------------------------------------------------------------------------- /tests/cycle_initial_call_query.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! It's possible to call a Salsa query from within a cycle initial fn. 4 | 5 | #[salsa::tracked] 6 | fn initial_value(_db: &dyn salsa::Database) -> u32 { 7 | 0 8 | } 9 | 10 | #[salsa::tracked(cycle_initial=cycle_initial)] 11 | fn query(db: &dyn salsa::Database) -> u32 { 12 | let val = query(db); 13 | if val < 5 { 14 | val + 1 15 | } else { 16 | val 17 | } 18 | } 19 | 20 | fn cycle_initial(db: &dyn salsa::Database, _id: salsa::Id) -> u32 { 21 | initial_value(db) 22 | } 23 | 24 | #[test_log::test] 25 | fn the_test() { 26 | let db = salsa::DatabaseImpl::default(); 27 | 28 | assert_eq!(query(&db), 5); 29 | } 30 | -------------------------------------------------------------------------------- /tests/tracked_fn_constant.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! Test that a constant `tracked` fn (has no inputs) 4 | //! compiles and executes successfully. 5 | #![allow(warnings)] 6 | 7 | use crate::common::LogDatabase; 8 | 9 | mod common; 10 | 11 | #[salsa::tracked] 12 | fn tracked_fn(db: &dyn salsa::Database) -> u32 { 13 | 44 14 | } 15 | 16 | #[salsa::tracked] 17 | fn tracked_custom_db(db: &dyn LogDatabase) -> u32 { 18 | 44 19 | } 20 | 21 | #[test] 22 | fn execute() { 23 | let mut db = salsa::DatabaseImpl::new(); 24 | assert_eq!(tracked_fn(&db), 44); 25 | } 26 | 27 | #[test] 28 | fn execute_custom() { 29 | let mut db = common::LoggerDatabase::default(); 30 | assert_eq!(tracked_custom_db(&db), 44); 31 | } 32 | -------------------------------------------------------------------------------- /tests/cycle_result_dependencies.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | use salsa::{Database, Setter}; 4 | 5 | #[salsa::input] 6 | struct Input { 7 | value: i32, 8 | } 9 | 10 | #[salsa::tracked(cycle_result=cycle_result)] 11 | fn has_cycle(db: &dyn Database, input: Input) -> i32 { 12 | has_cycle(db, input) 13 | } 14 | 15 | fn cycle_result(db: &dyn Database, _id: salsa::Id, input: Input) -> i32 { 16 | input.value(db) 17 | } 18 | 19 | #[test] 20 | fn cycle_result_dependencies_are_recorded() { 21 | let mut db = salsa::DatabaseImpl::default(); 22 | let input = Input::new(&db, 123); 23 | assert_eq!(has_cycle(&db, input), 123); 24 | 25 | input.set_value(&mut db).to(456); 26 | assert_eq!(has_cycle(&db, input), 456); 27 | } 28 | -------------------------------------------------------------------------------- /tests/tracked_fn_multiple_args.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! Test that a `tracked` fn on multiple salsa struct args 4 | //! compiles and executes successfully. 5 | 6 | #[salsa::input] 7 | struct MyInput { 8 | field: u32, 9 | } 10 | 11 | #[salsa::interned] 12 | struct MyInterned<'db> { 13 | field: u32, 14 | } 15 | 16 | #[salsa::tracked] 17 | fn tracked_fn<'db>(db: &'db dyn salsa::Database, input: MyInput, interned: MyInterned<'db>) -> u32 { 18 | input.field(db) + interned.field(db) 19 | } 20 | 21 | #[test] 22 | fn execute() { 23 | let db = salsa::DatabaseImpl::new(); 24 | let input = MyInput::new(&db, 22); 25 | let interned = MyInterned::new(&db, 33); 26 | assert_eq!(tracked_fn(&db, input, interned), 55); 27 | } 28 | -------------------------------------------------------------------------------- /tests/tracked_fn_orphan_escape_hatch.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! Test that a `tracked` fn on a `salsa::input` 4 | //! compiles and executes successfully. 5 | #![allow(warnings)] 6 | 7 | use std::marker::PhantomData; 8 | 9 | #[salsa::input] 10 | struct MyInput { 11 | field: u32, 12 | } 13 | 14 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 15 | struct NotUpdate<'a>(PhantomData &'a ()>); 16 | 17 | #[salsa::tracked(unsafe(non_update_types))] 18 | fn tracked_fn(db: &dyn salsa::Database, input: MyInput) -> NotUpdate<'_> { 19 | NotUpdate(PhantomData) 20 | } 21 | 22 | #[test] 23 | fn execute() { 24 | let mut db = salsa::DatabaseImpl::new(); 25 | let input = MyInput::new(&db, 22); 26 | tracked_fn(&db, input); 27 | } 28 | -------------------------------------------------------------------------------- /tests/tracked_fn_return_ref.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | use salsa::Database; 4 | 5 | #[salsa::input] 6 | struct Input { 7 | number: usize, 8 | } 9 | 10 | #[salsa::tracked(returns(ref))] 11 | fn test(db: &dyn salsa::Database, input: Input) -> Vec { 12 | (0..input.number(db)).map(|i| format!("test {i}")).collect() 13 | } 14 | 15 | #[test] 16 | fn invoke() { 17 | salsa::DatabaseImpl::new().attach(|db| { 18 | let input = Input::new(db, 3); 19 | let x: &Vec = test(db, input); 20 | expect_test::expect![[r#" 21 | [ 22 | "test 0", 23 | "test 1", 24 | "test 2", 25 | ] 26 | "#]] 27 | .assert_debug_eq(x); 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /tests/compile-fail/invalid_return_mode.stderr: -------------------------------------------------------------------------------- 1 | error: Invalid return mode. Allowed modes are: ["copy", "clone", "ref", "deref", "as_ref", "as_deref"] 2 | --> tests/compile-fail/invalid_return_mode.rs:9:26 3 | | 4 | 9 | #[salsa::tracked(returns(not_a_return_mode))] 5 | | ^^^^^^^^^^^^^^^^^ 6 | 7 | error: Invalid return mode. Allowed modes are: ["copy", "clone", "ref", "deref", "as_ref", "as_deref"] 8 | --> tests/compile-fail/invalid_return_mode.rs:16:15 9 | | 10 | 16 | #[returns(not_a_return_mode)] 11 | | ^^^^^^^^^^^^^^^^^ 12 | 13 | error: cannot find attribute `returns` in this scope 14 | --> tests/compile-fail/invalid_return_mode.rs:16:7 15 | | 16 | 16 | #[returns(not_a_return_mode)] 17 | | ^^^^^^^ 18 | -------------------------------------------------------------------------------- /book/src/plumbing/terminology/untracked.md: -------------------------------------------------------------------------------- 1 | # Untracked dependency 2 | 3 | An *untracked dependency* is an indication that the result of a [derived query] depends on something not visible to the salsa database. Untracked dependencies are created by invoking [`report_untracked_read`](https://docs.rs/salsa/0.16.1/salsa/struct.Runtime.html#method.report_untracked_read) or [`report_synthetic_read`](https://docs.rs/salsa/0.16.1/salsa/struct.Runtime.html#method.report_synthetic_read). When an untracked dependency is present, [derived queries] are always re-executed if the durability check fails (see the description of the [fetch operation] for more details). 4 | 5 | [derived query]: ./derived_query.md 6 | [derived queries]: ./derived_query.md 7 | [fetch operation]: ../fetch.md#derived-queries 8 | -------------------------------------------------------------------------------- /components/salsa-macro-rules/src/macro_if.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! macro_if { 3 | (true => $($t:tt)*) => { 4 | $($t)* 5 | }; 6 | 7 | (false => $($t:tt)*) => { 8 | }; 9 | 10 | (if true { $($t:tt)* } else { $($f:tt)*}) => { 11 | $($t)* 12 | }; 13 | 14 | (if false { $($t:tt)* } else { $($f:tt)*}) => { 15 | $($f)* 16 | }; 17 | 18 | (if0 0 { $($t:tt)* } else { $($f:tt)*}) => { 19 | $($t)* 20 | }; 21 | 22 | (if0 $n:literal { $($t:tt)* } else { $($f:tt)*}) => { 23 | $($f)* 24 | }; 25 | 26 | (iftt () { $($t:tt)* } else { $($f:tt)*}) => { 27 | $($f)* 28 | }; 29 | 30 | (iftt ($($tt:tt)+) { $($t:tt)* } else { $($f:tt)*}) => { 31 | $($t)* 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /tests/compile-fail/span-input-setter.stderr: -------------------------------------------------------------------------------- 1 | error[E0308]: mismatched types 2 | --> tests/compile-fail/span-input-setter.rs:10:21 3 | | 4 | 10 | input.set_field(22); 5 | | --------- ^^ expected `&mut _`, found integer 6 | | | 7 | | arguments to this method are incorrect 8 | | 9 | = note: expected mutable reference `&mut _` 10 | found type `{integer}` 11 | note: method defined here 12 | --> tests/compile-fail/span-input-setter.rs:3:5 13 | | 14 | 1 | #[salsa::input] 15 | | --------------- 16 | 2 | pub struct MyInput { 17 | 3 | field: u32, 18 | | ^^^^^ 19 | help: consider mutably borrowing here 20 | | 21 | 10 | input.set_field(&mut 22); 22 | | ++++ 23 | -------------------------------------------------------------------------------- /tests/compile-fail/tracked_struct_incompatibles.rs: -------------------------------------------------------------------------------- 1 | #[salsa::tracked(returns(ref))] 2 | struct TrackedWithRetRef { 3 | field: u32, 4 | } 5 | 6 | #[salsa::tracked(specify)] 7 | struct TrackedSructWithSpecify { 8 | field: u32, 9 | } 10 | 11 | #[salsa::tracked(no_eq)] 12 | struct TrackedStructWithNoEq { 13 | field: u32, 14 | } 15 | 16 | #[salsa::tracked(db = Db)] 17 | struct TrackedStructWithDb { 18 | field: u32, 19 | } 20 | 21 | #[salsa::tracked(recover_fn = recover)] 22 | struct TrackedStructWithRecover { 23 | field: u32, 24 | } 25 | 26 | #[salsa::tracked(lru = 12)] 27 | struct TrackedStructWithLru { 28 | field: u32, 29 | } 30 | 31 | #[salsa::tracked(revisions = 12)] 32 | struct TrackedStructWithRevisions { 33 | field: u32, 34 | } 35 | 36 | fn main() {} 37 | -------------------------------------------------------------------------------- /tests/hash_collision.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | use std::hash::Hash; 4 | 5 | #[test] 6 | fn hello() { 7 | use salsa::{Database, DatabaseImpl, Setter}; 8 | 9 | #[salsa::input] 10 | struct Bool { 11 | value: bool, 12 | } 13 | 14 | #[salsa::tracked] 15 | struct True<'db> {} 16 | 17 | #[salsa::tracked] 18 | struct False<'db> {} 19 | 20 | #[salsa::tracked] 21 | fn hello(db: &dyn Database, bool: Bool) { 22 | if bool.value(db) { 23 | True::new(db); 24 | } else { 25 | False::new(db); 26 | } 27 | } 28 | 29 | let mut db = DatabaseImpl::new(); 30 | let input = Bool::new(&db, false); 31 | hello(&db, input); 32 | input.set_value(&mut db).to(true); 33 | hello(&db, input); 34 | } 35 | -------------------------------------------------------------------------------- /tests/compile-fail/tracked_method_incompatibles.rs: -------------------------------------------------------------------------------- 1 | #[salsa::tracked] 2 | struct Tracked<'db> { 3 | field: u32, 4 | } 5 | 6 | #[salsa::tracked] 7 | impl<'db> Tracked<'db> { 8 | #[salsa::tracked] 9 | fn ref_self(&self, db: &dyn salsa::Database) {} 10 | } 11 | 12 | #[salsa::tracked] 13 | impl<'db> Tracked<'db> { 14 | #[salsa::tracked] 15 | fn ref_mut_self(&mut self, db: &dyn salsa::Database) {} 16 | } 17 | 18 | #[salsa::tracked] 19 | impl<'db> Tracked<'db> { 20 | #[salsa::tracked] 21 | fn multiple_lifetimes<'db1>(&mut self, db: &'db1 dyn salsa::Database) {} 22 | } 23 | 24 | #[salsa::tracked] 25 | impl<'db> Tracked<'db> { 26 | #[salsa::tracked] 27 | fn type_generics(&mut self, db: &dyn salsa::Database) -> T { 28 | panic!() 29 | } 30 | } 31 | 32 | fn main() {} 33 | -------------------------------------------------------------------------------- /tests/cycle_initial_call_back_into_cycle.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! Calling back into the same cycle from your cycle initial function will trigger another cycle. 4 | 5 | #[salsa::tracked] 6 | fn initial_value(db: &dyn salsa::Database) -> u32 { 7 | query(db) 8 | } 9 | 10 | #[salsa::tracked(cycle_initial=cycle_initial)] 11 | fn query(db: &dyn salsa::Database) -> u32 { 12 | let val = query(db); 13 | if val < 5 { 14 | val + 1 15 | } else { 16 | val 17 | } 18 | } 19 | 20 | fn cycle_initial(db: &dyn salsa::Database, _id: salsa::Id) -> u32 { 21 | initial_value(db) 22 | } 23 | 24 | #[test_log::test] 25 | #[should_panic(expected = "dependency graph cycle")] 26 | fn the_test() { 27 | let db = salsa::DatabaseImpl::default(); 28 | 29 | query(&db); 30 | } 31 | -------------------------------------------------------------------------------- /tests/intern_access_in_different_revision.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | use salsa::{Durability, Setter}; 4 | 5 | #[salsa::interned(no_lifetime)] 6 | struct Interned { 7 | field: u32, 8 | } 9 | 10 | #[salsa::input] 11 | struct Input { 12 | field: i32, 13 | } 14 | 15 | #[test] 16 | fn the_test() { 17 | let mut db = salsa::DatabaseImpl::default(); 18 | let input = Input::builder(-123456) 19 | .field_durability(Durability::HIGH) 20 | .new(&db); 21 | // Create an intern in an early revision. 22 | let interned = Interned::new(&db, 0xDEADBEEF); 23 | // Trigger a new revision. 24 | input 25 | .set_field(&mut db) 26 | .with_durability(Durability::HIGH) 27 | .to(123456); 28 | // Read the interned value 29 | let _ = interned.field(&db); 30 | } 31 | -------------------------------------------------------------------------------- /tests/compile-fail/accumulator_incompatibles.rs: -------------------------------------------------------------------------------- 1 | #[salsa::accumulator(returns(ref))] 2 | struct AccWithRetRef(u32); 3 | 4 | #[salsa::accumulator(specify)] 5 | struct AccWithSpecify(u32); 6 | 7 | #[salsa::accumulator(no_eq)] 8 | struct AccWithNoEq(u32); 9 | 10 | #[salsa::accumulator(data = MyAcc)] 11 | struct AccWithData(u32); 12 | 13 | #[salsa::accumulator(db = Db)] 14 | struct AcWithcDb(u32); 15 | 16 | #[salsa::accumulator(recover_fn = recover)] 17 | struct AccWithRecover(u32); 18 | 19 | #[salsa::accumulator(lru = 12)] 20 | struct AccWithLru(u32); 21 | 22 | #[salsa::accumulator(revisions = 12)] 23 | struct AccWithRevisions(u32); 24 | 25 | #[salsa::accumulator(constructor = Constructor)] 26 | struct AccWithConstructor(u32); 27 | 28 | #[salsa::accumulator(heap_size = size)] 29 | struct AccWithHeapSize(u32); 30 | 31 | fn main() {} 32 | -------------------------------------------------------------------------------- /tests/tracked_method_inherent_return_ref.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | use salsa::Database; 4 | 5 | #[salsa::input] 6 | struct Input { 7 | number: usize, 8 | } 9 | 10 | #[salsa::tracked] 11 | impl Input { 12 | #[salsa::tracked(returns(ref))] 13 | fn test(self, db: &dyn salsa::Database) -> Vec { 14 | (0..self.number(db)).map(|i| format!("test {i}")).collect() 15 | } 16 | } 17 | 18 | #[test] 19 | fn invoke() { 20 | salsa::DatabaseImpl::new().attach(|db| { 21 | let input = Input::new(db, 3); 22 | let x: &Vec = input.test(db); 23 | expect_test::expect![[r#" 24 | [ 25 | "test 0", 26 | "test 1", 27 | "test 2", 28 | ] 29 | "#]] 30 | .assert_debug_eq(x); 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /tests/tracked_method_inherent_return_deref.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | use salsa::Database; 4 | 5 | #[salsa::input] 6 | struct Input { 7 | number: usize, 8 | } 9 | 10 | #[salsa::tracked] 11 | impl Input { 12 | #[salsa::tracked(returns(deref))] 13 | fn test(self, db: &dyn salsa::Database) -> Vec { 14 | (0..self.number(db)).map(|i| format!("test {i}")).collect() 15 | } 16 | } 17 | 18 | #[test] 19 | fn invoke() { 20 | salsa::DatabaseImpl::new().attach(|db| { 21 | let input = Input::new(db, 3); 22 | let x: &[String] = input.test(db); 23 | 24 | assert_eq!( 25 | x, 26 | &[ 27 | "test 0".to_string(), 28 | "test 1".to_string(), 29 | "test 2".to_string() 30 | ] 31 | ); 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /tests/compile-fail/tracked_method_on_untracked_impl.stderr: -------------------------------------------------------------------------------- 1 | error: #[salsa::tracked] must also be applied to the impl block for tracked methods 2 | --> tests/compile-fail/tracked_method_on_untracked_impl.rs:8:41 3 | | 4 | 8 | fn tracked_method_on_untracked_impl(self, db: &dyn Db) -> u32 { 5 | | ^^^^ 6 | 7 | error[E0405]: cannot find trait `Db` in this scope 8 | --> tests/compile-fail/tracked_method_on_untracked_impl.rs:8:56 9 | | 10 | 8 | fn tracked_method_on_untracked_impl(self, db: &dyn Db) -> u32 { 11 | | ^^ not found in this scope 12 | 13 | error[E0425]: cannot find value `input` in this scope 14 | --> tests/compile-fail/tracked_method_on_untracked_impl.rs:9:9 15 | | 16 | 9 | input.field(db) 17 | | ^^^^^ not found in this scope 18 | -------------------------------------------------------------------------------- /tests/compile-fail/interned_struct_incompatibles.rs: -------------------------------------------------------------------------------- 1 | #[salsa::interned(returns(ref))] 2 | struct InternedWithRetRef { 3 | field: u32, 4 | } 5 | 6 | #[salsa::interned(specify)] 7 | struct InternedWithSpecify { 8 | field: u32, 9 | } 10 | 11 | #[salsa::interned(no_eq)] 12 | struct InternedWithNoEq { 13 | field: u32, 14 | } 15 | 16 | #[salsa::interned(db = Db)] 17 | struct InternedWithDb { 18 | field: u32, 19 | } 20 | 21 | #[salsa::interned(recover_fn = recover)] 22 | struct InternedWithRecover { 23 | field: u32, 24 | } 25 | 26 | #[salsa::interned(lru = 12)] 27 | struct InternedWithLru { 28 | field: u32, 29 | } 30 | 31 | #[salsa::interned] 32 | struct InternedWithTrackedField { 33 | #[tracked] 34 | field: u32, 35 | } 36 | 37 | #[salsa::interned(revisions = 0)] 38 | struct InternedWithZeroRevisions { 39 | field: u32, 40 | } 41 | 42 | fn main() {} 43 | -------------------------------------------------------------------------------- /book/src/videos.md: -------------------------------------------------------------------------------- 1 | # Videos 2 | 3 | There is currently one video available on the newest version of Salsa: 4 | 5 | - [Salsa Architecture Walkthrough](https://www.youtube.com/watch?v=vrnNvAAoQFk), 6 | which covers many aspects of the redesigned architecture. 7 | 8 | There are also two videos on the older version Salsa, but they are rather 9 | outdated: 10 | 11 | - [How Salsa Works](https://youtu.be/_muY4HjSqVw), which gives a high-level 12 | introduction to the key concepts involved and shows how to use Salsa; 13 | - [Salsa In More Depth](https://www.youtube.com/watch?v=i_IhACacPRY), which digs 14 | into the incremental algorithm and explains -- at a high-level -- how Salsa is 15 | implemented. 16 | 17 | > If you're in China, watch videos on 18 | > [How Salsa Works](https://www.bilibili.com/video/BV1Df4y1A7t3/), 19 | > [Salsa In More Depth](https://www.bilibili.com/video/BV1AM4y1G7E4/). 20 | -------------------------------------------------------------------------------- /book/src/about_salsa.md: -------------------------------------------------------------------------------- 1 | # About salsa 2 | 3 | Salsa is a Rust framework for writing incremental, on-demand programs 4 | -- these are programs that want to adapt to changes in their inputs, 5 | continuously producing a new output that is up-to-date. Salsa is based 6 | on the incremental recompilation techniques that we built for 7 | rustc, and many (but not all) of its users are building compilers or 8 | other similar tooling. 9 | 10 | If you'd like to learn more about Salsa, check out: 11 | 12 | - The [overview](./overview.md), for a brief summary. 13 | - The [tutorial](./tutorial.md), for a detailed look. 14 | - You can also watch some of our [videos](./videos.md), though the content there is rather out of date. 15 | 16 | If you'd like to chat about Salsa, or you think you might like to 17 | contribute, please jump on to our Zulip instance at 18 | [salsa.zulipchat.com](https://salsa.zulipchat.com/). 19 | -------------------------------------------------------------------------------- /components/salsa-macro-rules/src/maybe_default.rs: -------------------------------------------------------------------------------- 1 | /// Generate either `field_ref_expr` or `field_ty::default` 2 | /// 3 | /// Used when generating an input's builder. 4 | #[macro_export] 5 | macro_rules! maybe_default { 6 | ( 7 | ($return_mode:ident, $maybe_backdate:ident, default), 8 | $field_ty:ty, 9 | $field_ref_expr:expr, 10 | ) => { 11 | <$field_ty>::default() 12 | }; 13 | 14 | ( 15 | ($return_mode:ident, $maybe_backdate:ident, required), 16 | $field_ty:ty, 17 | $field_ref_expr:expr, 18 | ) => { 19 | $field_ref_expr 20 | }; 21 | } 22 | 23 | #[macro_export] 24 | macro_rules! maybe_default_tt { 25 | (($return_mode:ident, $maybe_backdate:ident, default) => $($t:tt)*) => { 26 | $($t)* 27 | }; 28 | 29 | (($return_mode:ident, $maybe_backdate:ident, required) => $($t:tt)*) => { 30 | 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /components/salsa-macro-rules/src/unexpected_cycle_recovery.rs: -------------------------------------------------------------------------------- 1 | // Macro that generates the body of the cycle recovery function 2 | // for the case where no cycle recovery is possible. This has to be 3 | // a macro because it can take a variadic number of arguments. 4 | #[macro_export] 5 | macro_rules! unexpected_cycle_recovery { 6 | ($db:ident, $cycle:ident, $last_provisional_value:ident, $new_value:ident, $($other_inputs:ident),*) => {{ 7 | let (_db, _cycle, _last_provisional_value) = ($db, $cycle, $last_provisional_value); 8 | std::mem::drop(($($other_inputs,)*)); 9 | $new_value 10 | }}; 11 | } 12 | 13 | #[macro_export] 14 | macro_rules! unexpected_cycle_initial { 15 | ($db:ident, $id:ident, $($other_inputs:ident),*) => {{ 16 | std::mem::drop($db); 17 | std::mem::drop(($($other_inputs,)*)); 18 | panic!("no cycle initial value") 19 | }}; 20 | } 21 | -------------------------------------------------------------------------------- /components/salsa-macros/src/tracked.rs: -------------------------------------------------------------------------------- 1 | use syn::spanned::Spanned; 2 | use syn::Item; 3 | 4 | use crate::token_stream_with_error; 5 | 6 | pub(crate) fn tracked( 7 | args: proc_macro::TokenStream, 8 | input: proc_macro::TokenStream, 9 | ) -> proc_macro::TokenStream { 10 | let item = parse_macro_input!(input as Item); 11 | let res = match item { 12 | syn::Item::Struct(item) => crate::tracked_struct::tracked_struct(args, item), 13 | syn::Item::Fn(item) => crate::tracked_fn::tracked_fn(args, item), 14 | syn::Item::Impl(item) => crate::tracked_impl::tracked_impl(args, item), 15 | _ => Err(syn::Error::new( 16 | item.span(), 17 | "tracked can only be applied to structs, functions, and impls", 18 | )), 19 | }; 20 | match res { 21 | Ok(s) => s.into(), 22 | Err(err) => token_stream_with_error(input, err), 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/cycle_recovery_call_query.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! It's possible to call a Salsa query from within a cycle recovery fn. 4 | 5 | #[salsa::tracked] 6 | fn fallback_value(_db: &dyn salsa::Database) -> u32 { 7 | 10 8 | } 9 | 10 | #[salsa::tracked(cycle_fn=cycle_fn, cycle_initial=cycle_initial)] 11 | fn query(db: &dyn salsa::Database) -> u32 { 12 | let val = query(db); 13 | if val < 5 { 14 | val + 1 15 | } else { 16 | val 17 | } 18 | } 19 | 20 | fn cycle_initial(_db: &dyn salsa::Database, _id: salsa::Id) -> u32 { 21 | 0 22 | } 23 | 24 | fn cycle_fn( 25 | db: &dyn salsa::Database, 26 | _cycle: &salsa::Cycle, 27 | _last_provisional_value: &u32, 28 | _value: u32, 29 | ) -> u32 { 30 | fallback_value(db) 31 | } 32 | 33 | #[test_log::test] 34 | fn the_test() { 35 | let db = salsa::DatabaseImpl::default(); 36 | 37 | assert_eq!(query(&db), 10); 38 | } 39 | -------------------------------------------------------------------------------- /tests/tracked_method_trait_return_ref.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | use salsa::Database; 4 | 5 | #[salsa::input] 6 | struct Input { 7 | number: usize, 8 | } 9 | 10 | trait Trait { 11 | fn test(self, db: &dyn salsa::Database) -> &Vec; 12 | } 13 | 14 | #[salsa::tracked] 15 | impl Trait for Input { 16 | #[salsa::tracked(returns(ref))] 17 | fn test(self, db: &dyn salsa::Database) -> Vec { 18 | (0..self.number(db)).map(|i| format!("test {i}")).collect() 19 | } 20 | } 21 | 22 | #[test] 23 | fn invoke() { 24 | salsa::DatabaseImpl::new().attach(|db| { 25 | let input = Input::new(db, 3); 26 | let x: &Vec = input.test(db); 27 | expect_test::expect![[r#" 28 | [ 29 | "test 0", 30 | "test 1", 31 | "test 2", 32 | ] 33 | "#]] 34 | .assert_debug_eq(x); 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /book/netlify.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Script meant to be run from netlify 4 | 5 | set -x 6 | 7 | MDBOOK_VERSION='0.4.12' 8 | MDBOOK_LINKCHECK_VERSION='0.7.4' 9 | MDBOOK_MERMAID_VERSION='0.8.3' 10 | 11 | curl -L https://github.com/rust-lang/mdBook/releases/download/v$MDBOOK_VERSION/mdbook-v$MDBOOK_VERSION-x86_64-unknown-linux-gnu.tar.gz | tar xz -C ~/.cargo/bin 12 | curl -L https://github.com/badboy/mdbook-mermaid/releases/download/v$MDBOOK_MERMAID_VERSION/mdbook-mermaid-v$MDBOOK_MERMAID_VERSION-x86_64-unknown-linux-gnu.tar.gz | tar xz -C ~/.cargo/bin 13 | curl -L https://github.com/Michael-F-Bryan/mdbook-linkcheck/releases/download/v$MDBOOK_LINKCHECK_VERSION/mdbook-linkcheck.v$MDBOOK_LINKCHECK_VERSION.x86_64-unknown-linux-gnu.zip -O 14 | unzip mdbook-linkcheck.v$MDBOOK_LINKCHECK_VERSION.x86_64-unknown-linux-gnu.zip -d ~/.cargo/bin 15 | chmod +x ~/.cargo/bin/mdbook-linkcheck 16 | 17 | mdbook build 18 | mkdir versions 19 | mv book/html/* versions 20 | -------------------------------------------------------------------------------- /book/src/tutorial.md: -------------------------------------------------------------------------------- 1 | # Tutorial: calc 2 | 3 | This tutorial walks through an end-to-end example of using Salsa. 4 | It does not assume you know anything about salsa, 5 | but reading the [overview](./overview.md) first is probably a good idea to get familiar with the basic concepts. 6 | 7 | Our goal is define a compiler/interpreter for a simple language called `calc`. 8 | The `calc` compiler takes programs like the following and then parses and executes them: 9 | 10 | ``` 11 | fn area_rectangle(w, h) = w * h 12 | fn area_circle(r) = 3.14 * r * r 13 | print area_rectangle(3, 4) 14 | print area_circle(1) 15 | print 11 * 2 16 | ``` 17 | 18 | When executed, this program prints `12`, `3.14`, and `22`. 19 | 20 | If the program contains errors (e.g., a reference to an undefined function), it prints those out too. 21 | And, of course, it will be reactive, so small changes to the input don't require recompiling (or rexecuting, necessarily) the entire thing. 22 | -------------------------------------------------------------------------------- /tests/tracked-struct-unchanged-in-new-rev.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | use salsa::{Database as Db, Setter}; 4 | use test_log::test; 5 | 6 | #[salsa::input] 7 | struct MyInput { 8 | field: u32, 9 | } 10 | 11 | #[salsa::tracked] 12 | struct MyTracked<'db> { 13 | field: u32, 14 | } 15 | 16 | #[salsa::tracked] 17 | fn tracked_fn(db: &dyn Db, input: MyInput) -> MyTracked<'_> { 18 | MyTracked::new(db, input.field(db) / 2) 19 | } 20 | 21 | #[test] 22 | fn execute() { 23 | let mut db = salsa::DatabaseImpl::new(); 24 | 25 | let input1 = MyInput::new(&db, 22); 26 | let input2 = MyInput::new(&db, 44); 27 | let _tracked1 = tracked_fn(&db, input1); 28 | let _tracked2 = tracked_fn(&db, input2); 29 | 30 | // modify the input and change the revision 31 | input1.set_field(&mut db).to(24); 32 | let tracked2 = tracked_fn(&db, input2); 33 | 34 | // this should not panic 35 | tracked2.field(&db); 36 | } 37 | -------------------------------------------------------------------------------- /tests/mutate_in_place.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! Test that a setting a field on a `#[salsa::input]` 4 | //! overwrites and returns the old value. 5 | 6 | use salsa::Setter; 7 | use test_log::test; 8 | 9 | #[salsa::input] 10 | struct MyInput { 11 | field: String, 12 | } 13 | 14 | #[test] 15 | fn execute() { 16 | let mut db = salsa::DatabaseImpl::new(); 17 | 18 | let input = MyInput::new(&db, "Hello".to_string()); 19 | 20 | // Overwrite field with an empty String 21 | // and store the old value in my_string 22 | let mut my_string = input.set_field(&mut db).to(String::new()); 23 | my_string.push_str(" World!"); 24 | 25 | // Set the field back to out initial String, 26 | // expecting to get the empty one back 27 | assert_eq!(input.set_field(&mut db).to(my_string), ""); 28 | 29 | // Check if the stored String is the one we expected 30 | assert_eq!(input.field(&db), "Hello World!"); 31 | } 32 | -------------------------------------------------------------------------------- /tests/tracked_fn_high_durability_dependency.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | #![allow(warnings)] 3 | 4 | use salsa::plumbing::HasStorage; 5 | use salsa::{Database, Durability, Setter}; 6 | 7 | mod common; 8 | #[salsa::input] 9 | struct MyInput { 10 | field: u32, 11 | } 12 | 13 | #[salsa::tracked] 14 | fn tracked_fn(db: &dyn salsa::Database, input: MyInput) -> u32 { 15 | input.field(db) * 2 16 | } 17 | 18 | #[test] 19 | fn execute() { 20 | let mut db = salsa::DatabaseImpl::default(); 21 | 22 | let input_high = MyInput::new(&mut db, 0); 23 | input_high 24 | .set_field(&mut db) 25 | .with_durability(Durability::HIGH) 26 | .to(2200); 27 | 28 | assert_eq!(tracked_fn(&db, input_high), 4400); 29 | 30 | // Changing the value should re-execute the query 31 | input_high 32 | .set_field(&mut db) 33 | .with_durability(Durability::HIGH) 34 | .to(2201); 35 | 36 | assert_eq!(tracked_fn(&db, input_high), 4402); 37 | } 38 | -------------------------------------------------------------------------------- /book/src/plumbing/generated_code.md: -------------------------------------------------------------------------------- 1 | # Generated code 2 | 3 | This page walks through the ["Hello, World!"] example and explains the code that 4 | it generates. Please take it with a grain of salt: while we make an effort to 5 | keep this documentation up to date, this sort of thing can fall out of date 6 | easily. See the page history below for major updates. 7 | 8 | ["Hello, World!"]: https://github.com/salsa-rs/salsa/blob/master/examples/hello_world/main.rs 9 | 10 | If you'd like to see for yourself, you can set the environment variable 11 | `SALSA_DUMP` to 1 while the procedural macro runs, and it will dump the full 12 | output to stdout. I recommend piping the output through rustfmt. 13 | 14 | ## Sources 15 | 16 | The main parts of the source that we are focused on are as follows. 17 | 18 | ### Query group 19 | 20 | ```rust,ignore 21 | {{#include ../../../examples/hello_world/main.rs:trait}} 22 | ``` 23 | 24 | ### Database 25 | 26 | ```rust,ignore 27 | {{#include ../../../examples/hello_world/main.rs:database}} 28 | ``` 29 | -------------------------------------------------------------------------------- /src/hash.rs: -------------------------------------------------------------------------------- 1 | use std::hash::{BuildHasher, Hash, Hasher}; 2 | 3 | pub(crate) type FxHasher = std::hash::BuildHasherDefault; 4 | pub(crate) type FxIndexSet = indexmap::IndexSet; 5 | pub(crate) type FxLinkedHashSet = hashlink::LinkedHashSet; 6 | pub(crate) type FxHashSet = std::collections::HashSet; 7 | 8 | pub(crate) fn hash(t: &T) -> u64 { 9 | FxHasher::default().hash_one(t) 10 | } 11 | 12 | // `TypeId` is a 128-bit hash internally, and it's `Hash` implementation 13 | // writes the lower 64-bits. Hashing it again would be unnecessary. 14 | #[derive(Default)] 15 | pub(crate) struct TypeIdHasher(u64); 16 | 17 | impl Hasher for TypeIdHasher { 18 | fn write(&mut self, _: &[u8]) { 19 | unreachable!("`TypeId` calls `write_u64`"); 20 | } 21 | 22 | #[inline] 23 | fn write_u64(&mut self, id: u64) { 24 | self.0 = id; 25 | } 26 | 27 | #[inline] 28 | fn finish(&self) -> u64 { 29 | self.0 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/tracked_struct_recreate_new_revision.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! Test that re-creating a `tracked` struct after it was deleted in a previous 4 | //! revision doesn't panic. 5 | #![allow(warnings)] 6 | 7 | use salsa::Setter; 8 | 9 | #[salsa::input] 10 | struct MyInput { 11 | field: u32, 12 | } 13 | 14 | #[salsa::tracked(debug)] 15 | struct TrackedStruct<'db> { 16 | field: u32, 17 | } 18 | 19 | #[salsa::tracked] 20 | fn tracked_fn(db: &dyn salsa::Database, input: MyInput) -> Option> { 21 | if input.field(db) == 1 { 22 | Some(TrackedStruct::new(db, 1)) 23 | } else { 24 | None 25 | } 26 | } 27 | 28 | #[test] 29 | fn execute() { 30 | let mut db = salsa::DatabaseImpl::new(); 31 | let input = MyInput::new(&db, 1); 32 | assert!(tracked_fn(&db, input).is_some()); 33 | input.set_field(&mut db).to(0); 34 | assert_eq!(tracked_fn(&db, input), None); 35 | input.set_field(&mut db).to(1); 36 | assert!(tracked_fn(&db, input).is_some()); 37 | } 38 | -------------------------------------------------------------------------------- /tests/input_setter_preserves_durability.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | use salsa::plumbing::ZalsaDatabase; 4 | use salsa::{Durability, Setter}; 5 | use test_log::test; 6 | 7 | #[salsa::input] 8 | struct MyInput { 9 | required_field: bool, 10 | 11 | #[default] 12 | optional_field: usize, 13 | } 14 | 15 | #[test] 16 | fn execute() { 17 | let mut db = salsa::DatabaseImpl::new(); 18 | 19 | let input = MyInput::builder(true) 20 | .required_field_durability(Durability::HIGH) 21 | .new(&db); 22 | 23 | // Change the field value. It should preserve high durability. 24 | input.set_required_field(&mut db).to(false); 25 | 26 | let last_high_revision = db.zalsa().last_changed_revision(Durability::HIGH); 27 | 28 | // Changing the value again should **again** dump the high durability revision. 29 | input.set_required_field(&mut db).to(false); 30 | 31 | assert_ne!( 32 | db.zalsa().last_changed_revision(Durability::HIGH), 33 | last_high_revision 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /tests/parallel/main.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | mod setup; 4 | mod signal; 5 | 6 | mod cycle_a_t1_b_t2; 7 | mod cycle_a_t1_b_t2_fallback; 8 | mod cycle_ab_peeping_c; 9 | mod cycle_iteration_mismatch; 10 | mod cycle_nested_deep; 11 | mod cycle_nested_deep_conditional; 12 | mod cycle_nested_deep_conditional_changed; 13 | mod cycle_nested_deep_panic; 14 | mod cycle_nested_three_threads; 15 | mod cycle_nested_three_threads_changed; 16 | mod cycle_panic; 17 | mod cycle_provisional_depending_on_itself; 18 | 19 | #[cfg(not(feature = "shuttle"))] 20 | pub(crate) mod sync { 21 | pub use std::sync::*; 22 | pub use std::thread; 23 | 24 | pub fn check(f: impl Fn() + Send + Sync + 'static) { 25 | f(); 26 | } 27 | } 28 | 29 | #[cfg(feature = "shuttle")] 30 | pub(crate) mod sync { 31 | pub use shuttle::sync::*; 32 | pub use shuttle::thread; 33 | 34 | pub fn check(f: impl Fn() + Send + Sync + 'static) { 35 | shuttle::check_pct(f, 2500, 50); 36 | } 37 | } 38 | 39 | pub(crate) use setup::*; 40 | -------------------------------------------------------------------------------- /tests/tracked_with_struct_ord.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! Test that `PartialOrd` and `Ord` can be derived for tracked structs 4 | 5 | use salsa::{Database, DatabaseImpl}; 6 | use test_log::test; 7 | 8 | #[salsa::input] 9 | #[derive(PartialOrd, Ord)] 10 | struct Input { 11 | value: usize, 12 | } 13 | 14 | #[salsa::tracked(debug)] 15 | #[derive(Ord, PartialOrd)] 16 | struct MyTracked<'db> { 17 | value: usize, 18 | } 19 | 20 | #[salsa::tracked] 21 | fn create_tracked(db: &dyn Database, input: Input) -> MyTracked<'_> { 22 | MyTracked::new(db, input.value(db)) 23 | } 24 | 25 | #[test] 26 | fn execute() { 27 | DatabaseImpl::new().attach(|db| { 28 | let input1 = Input::new(db, 20); 29 | let input2 = Input::new(db, 10); 30 | 31 | // Compares by ID and not by value. 32 | assert!(input1 <= input2); 33 | 34 | let t0: MyTracked = create_tracked(db, input1); 35 | let t1: MyTracked = create_tracked(db, input2); 36 | 37 | assert!(t0 <= t1); 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /tests/compile-fail/span-tracked-getter.stderr: -------------------------------------------------------------------------------- 1 | error[E0308]: mismatched types 2 | --> tests/compile-fail/span-tracked-getter.rs:9:13 3 | | 4 | 9 | x.field(22); 5 | | ----- ^^ expected `&_`, found integer 6 | | | 7 | | arguments to this method are incorrect 8 | | 9 | = note: expected reference `&_` 10 | found type `{integer}` 11 | note: method defined here 12 | --> tests/compile-fail/span-tracked-getter.rs:3:5 13 | | 14 | 1 | #[salsa::tracked] 15 | | ----------------- 16 | 2 | pub struct MyTracked<'db> { 17 | 3 | field: u32, 18 | | ^^^^^ 19 | help: consider borrowing here 20 | | 21 | 9 | x.field(&22); 22 | | + 23 | 24 | warning: variable does not need to be mutable 25 | --> tests/compile-fail/span-tracked-getter.rs:13:9 26 | | 27 | 13 | let mut db = salsa::DatabaseImpl::new(); 28 | | ----^^ 29 | | | 30 | | help: remove this `mut` 31 | | 32 | = note: `#[warn(unused_mut)]` (part of `#[warn(unused)]`) on by default 33 | -------------------------------------------------------------------------------- /tests/input_field_durability.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! Tests that code using the builder's durability methods compiles. 4 | 5 | use salsa::Durability; 6 | use test_log::test; 7 | 8 | #[salsa::input] 9 | struct MyInput { 10 | required_field: bool, 11 | 12 | #[default] 13 | optional_field: usize, 14 | } 15 | 16 | #[test] 17 | fn required_field_durability() { 18 | let db = salsa::DatabaseImpl::new(); 19 | 20 | let input = MyInput::builder(true) 21 | .required_field_durability(Durability::HIGH) 22 | .new(&db); 23 | 24 | assert!(input.required_field(&db)); 25 | assert_eq!(input.optional_field(&db), 0); 26 | } 27 | 28 | #[test] 29 | fn optional_field_durability() { 30 | let db = salsa::DatabaseImpl::new(); 31 | 32 | let input = MyInput::builder(true) 33 | .optional_field(20) 34 | .optional_field_durability(Durability::HIGH) 35 | .new(&db); 36 | 37 | assert!(input.required_field(&db)); 38 | assert_eq!(input.optional_field(&db), 20); 39 | } 40 | -------------------------------------------------------------------------------- /components/salsa-macro-rules/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate defines various `macro_rules` macros 2 | //! used as part of Salsa's internal plumbing. 3 | //! These macros are re-exported under `salsa::plumbing``. 4 | //! The procedural macros emit calls to these 5 | //! `macro_rules` macros after doing error checking. 6 | //! 7 | //! Using `macro_rules` macro definitions is generally 8 | //! more ergonomic and also permits true hygiene for local variables 9 | //! (sadly not items). 10 | //! 11 | //! Currently the only way to have a macro that is re-exported 12 | //! from a submodule is to use multiple crates, hence the existence 13 | //! of this crate. 14 | 15 | mod gate_accumulated; 16 | mod macro_if; 17 | mod maybe_backdate; 18 | mod maybe_default; 19 | mod return_mode; 20 | #[cfg(feature = "accumulator")] 21 | mod setup_accumulator_impl; 22 | mod setup_input_struct; 23 | mod setup_interned_struct; 24 | mod setup_tracked_assoc_fn_body; 25 | mod setup_tracked_fn; 26 | mod setup_tracked_method_body; 27 | mod setup_tracked_struct; 28 | mod unexpected_cycle_recovery; 29 | -------------------------------------------------------------------------------- /tests/debug_bounds.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! Test that debug and non-debug structs compile correctly 4 | 5 | #[derive(Ord, PartialOrd, Eq, PartialEq, Copy, Clone, Hash)] 6 | struct NotDebug; 7 | #[derive(Ord, PartialOrd, Eq, PartialEq, Copy, Clone, Hash, Debug)] 8 | struct Debug; 9 | 10 | #[salsa::input(debug)] 11 | struct DebugInput { 12 | field: Debug, 13 | } 14 | 15 | #[salsa::input] 16 | struct NotDebugInput { 17 | field: NotDebug, 18 | } 19 | 20 | #[salsa::interned(debug)] 21 | struct DebugInterned { 22 | field: Debug, 23 | } 24 | 25 | #[salsa::interned] 26 | struct NotDebugInterned { 27 | field: NotDebug, 28 | } 29 | 30 | #[salsa::interned(no_lifetime, debug)] 31 | struct DebugInternedNoLifetime { 32 | field: Debug, 33 | } 34 | 35 | #[salsa::interned(no_lifetime)] 36 | struct NotDebugInternedNoLifetime { 37 | field: NotDebug, 38 | } 39 | 40 | #[salsa::tracked(debug)] 41 | struct DebugTracked<'db> { 42 | field: Debug, 43 | } 44 | 45 | #[salsa::tracked] 46 | struct NotDebugTracked<'db> { 47 | field: NotDebug, 48 | } 49 | 50 | #[test] 51 | fn ok() {} 52 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /components/salsa-macro-rules/src/maybe_backdate.rs: -------------------------------------------------------------------------------- 1 | /// Conditionally update field value and backdate revisions 2 | #[macro_export] 3 | macro_rules! maybe_backdate { 4 | ( 5 | ($return_mode:ident, no_backdate, $maybe_default:ident), 6 | $maybe_update:tt, 7 | $old_field_place:expr, 8 | $new_field_place:expr, 9 | $revision_place:expr, 10 | $current_revision:expr, 11 | $zalsa:ident, 12 | 13 | ) => { 14 | $zalsa::always_update( 15 | &mut $revision_place, 16 | $current_revision, 17 | &mut $old_field_place, 18 | $new_field_place, 19 | ); 20 | }; 21 | 22 | ( 23 | ($return_mode:ident, backdate, $maybe_default:ident), 24 | $maybe_update:tt, 25 | $old_field_place:expr, 26 | $new_field_place:expr, 27 | $revision_place:expr, 28 | $current_revision:expr, 29 | $zalsa:ident, 30 | ) => { 31 | if $maybe_update(std::ptr::addr_of_mut!($old_field_place), $new_field_place) { 32 | $revision_place = $current_revision; 33 | } 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /tests/input_default.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! Tests that fields attributed with `#[default]` are initialized with `Default::default()`. 4 | 5 | use salsa::Durability; 6 | use test_log::test; 7 | 8 | #[salsa::input] 9 | struct MyInput { 10 | required: bool, 11 | #[default] 12 | optional: usize, 13 | } 14 | 15 | #[test] 16 | fn new_constructor() { 17 | let db = salsa::DatabaseImpl::new(); 18 | 19 | let input = MyInput::new(&db, true); 20 | 21 | assert!(input.required(&db)); 22 | assert_eq!(input.optional(&db), 0); 23 | } 24 | 25 | #[test] 26 | fn builder_specify_optional() { 27 | let db = salsa::DatabaseImpl::new(); 28 | 29 | let input = MyInput::builder(true).optional(20).new(&db); 30 | 31 | assert!(input.required(&db)); 32 | assert_eq!(input.optional(&db), 20); 33 | } 34 | 35 | #[test] 36 | fn builder_default_optional_value() { 37 | let db = salsa::DatabaseImpl::new(); 38 | 39 | let input = MyInput::builder(true) 40 | .required_durability(Durability::HIGH) 41 | .new(&db); 42 | 43 | assert!(input.required(&db)); 44 | assert_eq!(input.optional(&db), 0); 45 | } 46 | -------------------------------------------------------------------------------- /tests/compile-fail/tracked_fn_return_ref.rs: -------------------------------------------------------------------------------- 1 | use salsa::Database as Db; 2 | 3 | #[salsa::input] 4 | struct MyInput { 5 | #[returns(ref)] 6 | text: String, 7 | } 8 | 9 | #[derive(Clone, Debug, PartialEq, Eq)] 10 | struct ContainsRef<'db> { 11 | text: &'db str, 12 | } 13 | 14 | #[salsa::tracked] 15 | fn tracked_fn_return_ref<'db>(db: &'db dyn Db, input: MyInput) -> &'db str { 16 | input.text(db) 17 | } 18 | 19 | #[salsa::tracked] 20 | fn tracked_fn_return_struct_containing_ref<'db>( 21 | db: &'db dyn Db, 22 | input: MyInput, 23 | ) -> ContainsRef<'db> { 24 | ContainsRef { 25 | text: input.text(db), 26 | } 27 | } 28 | 29 | #[salsa::tracked] 30 | fn tracked_fn_return_struct_containing_ref_elided_implicit<'db>( 31 | db: &'db dyn Db, 32 | input: MyInput, 33 | ) -> ContainsRef { 34 | ContainsRef { 35 | text: input.text(db), 36 | } 37 | } 38 | 39 | #[salsa::tracked] 40 | fn tracked_fn_return_struct_containing_ref_elided_explicit<'db>( 41 | db: &'db dyn Db, 42 | input: MyInput, 43 | ) -> ContainsRef<'_> { 44 | ContainsRef { 45 | text: input.text(db), 46 | } 47 | } 48 | 49 | fn main() {} 50 | -------------------------------------------------------------------------------- /tests/tracked-struct-value-field-not-eq.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! Test a field whose `PartialEq` impl is always true. 4 | //! This can our "last changed" data to be wrong 5 | //! but we *should* always reflect the final values. 6 | 7 | use salsa::{Database, Setter}; 8 | use test_log::test; 9 | 10 | #[salsa::input] 11 | struct MyInput { 12 | field: bool, 13 | } 14 | 15 | #[derive(Hash, Debug, Clone)] 16 | struct NotEq { 17 | field: bool, 18 | } 19 | 20 | impl From for NotEq { 21 | fn from(value: bool) -> Self { 22 | Self { field: value } 23 | } 24 | } 25 | 26 | #[salsa::tracked] 27 | struct MyTracked<'db> { 28 | #[tracked] 29 | #[no_eq] 30 | field: NotEq, 31 | } 32 | 33 | #[salsa::tracked] 34 | fn the_fn(db: &dyn Database, input: MyInput) { 35 | let tracked0 = MyTracked::new(db, NotEq::from(input.field(db))); 36 | assert_eq!(tracked0.field(db).field, input.field(db)); 37 | } 38 | 39 | #[test] 40 | fn execute() { 41 | let mut db = salsa::DatabaseImpl::new(); 42 | 43 | let input = MyInput::new(&db, true); 44 | the_fn(&db, input); 45 | input.set_field(&mut db).to(false); 46 | the_fn(&db, input); 47 | } 48 | -------------------------------------------------------------------------------- /tests/compile-fail/invalid_persist_options.stderr: -------------------------------------------------------------------------------- 1 | error: option `serialize` provided twice 2 | --> tests/compile-fail/invalid_persist_options.rs:26:65 3 | | 4 | 26 | #[salsa::input(persist(serialize = serde::Serialize::serialize, serialize = serde::Serialize::serialize))] 5 | | ^^^^^^^^^ 6 | 7 | error: option `deserialize` provided twice 8 | --> tests/compile-fail/invalid_persist_options.rs:31:71 9 | | 10 | 31 | #[salsa::input(persist(deserialize = serde::Deserialize::deserialize, deserialize = serde::Deserialize::deserialize))] 11 | | ^^^^^^^^^^^ 12 | 13 | error: unexpected argument 14 | --> tests/compile-fail/invalid_persist_options.rs:36:24 15 | | 16 | 36 | #[salsa::input(persist(not_an_option = std::convert::identity))] 17 | | ^^^^^^^^^^^^^ 18 | 19 | error: unexpected argument 20 | --> tests/compile-fail/invalid_persist_options.rs:51:26 21 | | 22 | 51 | #[salsa::tracked(persist(serialize = serde::Serialize::serialize, deserialize = serde::Deserialize::deserialize))] 23 | | ^^^^^^^^^ 24 | -------------------------------------------------------------------------------- /tests/tracked-struct-id-field-bad-eq.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! Test an id field whose `PartialEq` impl is always true. 4 | 5 | use salsa::{Database, Setter}; 6 | use test_log::test; 7 | 8 | #[salsa::input] 9 | struct MyInput { 10 | field: bool, 11 | } 12 | 13 | #[allow(clippy::derived_hash_with_manual_eq)] 14 | #[derive(Eq, Hash, Debug, Clone)] 15 | struct BadEq { 16 | field: bool, 17 | } 18 | 19 | impl PartialEq for BadEq { 20 | fn eq(&self, _other: &Self) -> bool { 21 | true 22 | } 23 | } 24 | 25 | impl From for BadEq { 26 | fn from(value: bool) -> Self { 27 | Self { field: value } 28 | } 29 | } 30 | 31 | #[salsa::tracked] 32 | struct MyTracked<'db> { 33 | field: BadEq, 34 | } 35 | 36 | #[salsa::tracked] 37 | fn the_fn(db: &dyn Database, input: MyInput) { 38 | let tracked0 = MyTracked::new(db, BadEq::from(input.field(db))); 39 | assert_eq!(tracked0.field(db).field, input.field(db)); 40 | } 41 | 42 | #[test] 43 | fn execute() { 44 | let mut db = salsa::DatabaseImpl::new(); 45 | let input = MyInput::new(&db, true); 46 | the_fn(&db, input); 47 | input.set_field(&mut db).to(false); 48 | the_fn(&db, input); 49 | } 50 | -------------------------------------------------------------------------------- /book/src/tutorial/db.md: -------------------------------------------------------------------------------- 1 | # Defining the database struct 2 | 3 | First, we need to create the **database struct**. 4 | Typically it is only used by the "driver" of your application; 5 | the one which starts up the program, supplies the inputs, and relays the outputs. 6 | 7 | In `calc`, the database struct is in the [`db`] module, and it looks like this: 8 | 9 | [`db`]: https://github.com/salsa-rs/salsa/blob/master/examples/calc/db.rs 10 | 11 | ```rust 12 | {{#include ../../../examples/calc/db.rs:db_struct}} 13 | ``` 14 | 15 | The `#[salsa::db]` attribute marks the struct as a database. 16 | It must have a field named `storage` whose type is `salsa::Storage`, but it can also contain whatever other fields you want. 17 | 18 | ## Implementing the `salsa::Database` trait 19 | 20 | In addition to the struct itself, we must add an impl of `salsa::Database`: 21 | 22 | ```rust 23 | {{#include ../../../examples/calc/db.rs:db_impl}} 24 | ``` 25 | 26 | ## Implementing the `salsa::ParallelDatabase` trait 27 | 28 | If you want to permit accessing your database from multiple threads at once, then you also need to implement the `ParallelDatabase` trait: 29 | 30 | ```rust 31 | {{#include ../../../examples/calc/db.rs:par_db_impl}} 32 | ``` 33 | -------------------------------------------------------------------------------- /book/src/plumbing.md: -------------------------------------------------------------------------------- 1 | # Plumbing 2 | 3 | This chapter documents the code that salsa generates and its "inner workings". 4 | We refer to this as the "plumbing". 5 | 6 | ## Overview 7 | 8 | The plumbing section is broken up into chapters: 9 | 10 | - The [database and runtime](./plumbing/database_and_runtime.md) covers the data structures that are used at runtime to coordinate workers, trigger cancellation, track which functions are active and what dependencies they have accrued, and so forth. 11 | - The [query operations](./plumbing/query_ops.md) chapter describes how the major operations on function ingredients work. This text was written for an older version of salsa but the logic is the same: 12 | - The [maybe changed after](./plumbing/maybe_changed_after.md) operation determines when a memoized value for a tracked function is out of date. 13 | - The [fetch](./plumbing/fetch.md) operation computes the most recent value. 14 | - The [derived queries flowchart](./plumbing/derived_flowchart.md) depicts the logic in flowchart form. 15 | - The [cycle handling](./plumbing/cycles.md) handling chapter describes what happens when cycles occur. 16 | - The [terminology](./plumbing/terminology.md) section describes various words that appear throughout. 17 | -------------------------------------------------------------------------------- /book/src/plumbing/terminology/memo.md: -------------------------------------------------------------------------------- 1 | # Memo 2 | 3 | A *memo* stores information about the last time that a [query function] for some [query] Q was executed: 4 | 5 | * Typically, it contains the value that was returned from that function, so that we don't have to execute it again. 6 | * However, this is not always true: some queries don't cache their result values, and values can also be dropped as a result of [LRU] collection. In those cases, the memo just stores [dependency] information, which can still be useful to determine if other queries that have Q as a [dependency] may have changed. 7 | * The revision in which the memo last [verified]. 8 | * The [changed at] revision in which the memo's value last changed. (Note that it may be [backdated].) 9 | * The minimum durability of the memo's [dependencies]. 10 | * The complete set of [dependencies], if available, or a marker that the memo has an [untracked dependency]. 11 | 12 | [revision]: ./revision.md 13 | [backdated]: ./backdate.md 14 | [dependencies]: ./dependency.md 15 | [dependency]: ./dependency.md 16 | [untracked dependency]: ./untracked.md 17 | [verified]: ./verified.md 18 | [query]: ./query.md 19 | [query function]: ./query_function.md 20 | [changed at]: ./changed_at.md 21 | [LRU]: ./LRU.md -------------------------------------------------------------------------------- /tests/synthetic_write.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! Test that a constant `tracked` fn (has no inputs) 4 | //! compiles and executes successfully. 5 | #![allow(warnings)] 6 | 7 | mod common; 8 | 9 | use common::{LogDatabase, Logger}; 10 | use expect_test::expect; 11 | use salsa::{Database, DatabaseImpl, Durability, Event, EventKind}; 12 | 13 | #[salsa::input] 14 | struct MyInput { 15 | field: u32, 16 | } 17 | 18 | #[salsa::tracked] 19 | fn tracked_fn(db: &dyn Database, input: MyInput) -> u32 { 20 | input.field(db) * 2 21 | } 22 | 23 | #[test] 24 | fn execute() { 25 | let mut db = common::ExecuteValidateLoggerDatabase::default(); 26 | 27 | let input = MyInput::new(&db, 22); 28 | assert_eq!(tracked_fn(&db, input), 44); 29 | 30 | db.assert_logs(expect![[r#" 31 | [ 32 | "salsa_event(WillExecute { database_key: tracked_fn(Id(0)) })", 33 | ]"#]]); 34 | 35 | // Bumps the revision 36 | db.synthetic_write(Durability::LOW); 37 | 38 | // Query should re-run 39 | assert_eq!(tracked_fn(&db, input), 44); 40 | 41 | db.assert_logs(expect![[r#" 42 | [ 43 | "salsa_event(DidValidateMemoizedValue { database_key: tracked_fn(Id(0)) })", 44 | ]"#]]); 45 | } 46 | -------------------------------------------------------------------------------- /tests/accumulate-custom-debug.rs: -------------------------------------------------------------------------------- 1 | #![cfg(all(feature = "inventory", feature = "accumulator"))] 2 | 3 | mod common; 4 | 5 | use expect_test::expect; 6 | use salsa::{Accumulator, Database}; 7 | use test_log::test; 8 | 9 | #[salsa::input(debug)] 10 | struct MyInput { 11 | count: u32, 12 | } 13 | 14 | #[salsa::accumulator] 15 | struct Log(String); 16 | 17 | impl std::fmt::Debug for Log { 18 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 19 | f.debug_tuple("CustomLog").field(&self.0).finish() 20 | } 21 | } 22 | 23 | #[salsa::tracked] 24 | fn push_logs(db: &dyn salsa::Database, input: MyInput) { 25 | for i in 0..input.count(db) { 26 | Log(format!("#{i}")).accumulate(db); 27 | } 28 | } 29 | 30 | #[test] 31 | fn accumulate_custom_debug() { 32 | salsa::DatabaseImpl::new().attach(|db| { 33 | let input = MyInput::new(db, 2); 34 | let logs = push_logs::accumulated::(db, input); 35 | expect![[r##" 36 | [ 37 | CustomLog( 38 | "#0", 39 | ), 40 | CustomLog( 41 | "#1", 42 | ), 43 | ] 44 | "##]] 45 | .assert_debug_eq(&logs); 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /tests/check_auto_traits.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! Test that auto trait impls exist as expected. 4 | 5 | use std::panic::UnwindSafe; 6 | 7 | use salsa::Database; 8 | use test_log::test; 9 | 10 | #[salsa::input] 11 | struct MyInput { 12 | field: String, 13 | } 14 | 15 | #[salsa::tracked] 16 | struct MyTracked<'db> { 17 | field: MyInterned<'db>, 18 | } 19 | 20 | #[salsa::interned] 21 | struct MyInterned<'db> { 22 | field: String, 23 | } 24 | 25 | #[salsa::tracked] 26 | fn test(db: &dyn Database, input: MyInput) { 27 | let input = is_send(is_sync(input)); 28 | let interned = is_send(is_sync(MyInterned::new(db, input.field(db).clone()))); 29 | let _tracked_struct = is_send(is_sync(MyTracked::new(db, interned))); 30 | } 31 | 32 | fn is_send(t: T) -> T { 33 | t 34 | } 35 | 36 | fn is_sync(t: T) -> T { 37 | t 38 | } 39 | 40 | fn is_unwind_safe(t: T) -> T { 41 | t 42 | } 43 | 44 | #[test] 45 | fn execute() { 46 | let db = is_send(salsa::DatabaseImpl::new()); 47 | let _handle = is_send(is_sync(is_unwind_safe( 48 | db.storage().clone().into_zalsa_handle(), 49 | ))); 50 | let input = MyInput::new(&db, "Hello".to_string()); 51 | test(&db, input); 52 | } 53 | -------------------------------------------------------------------------------- /tests/cycle_regression_455.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | use salsa::{Database, Setter}; 4 | 5 | #[salsa::tracked] 6 | fn memoized(db: &dyn Database, input: MyInput) -> u32 { 7 | memoized_a(db, MyTracked::new(db, input.field(db))) 8 | } 9 | 10 | #[salsa::tracked(cycle_initial=cycle_initial)] 11 | fn memoized_a<'db>(db: &'db dyn Database, tracked: MyTracked<'db>) -> u32 { 12 | MyTracked::new(db, 0); 13 | memoized_b(db, tracked) 14 | } 15 | fn cycle_initial(_db: &dyn Database, _id: salsa::Id, _input: MyTracked) -> u32 { 16 | 0 17 | } 18 | 19 | #[salsa::tracked] 20 | fn memoized_b<'db>(db: &'db dyn Database, tracked: MyTracked<'db>) -> u32 { 21 | let incr = tracked.field(db); 22 | let a = memoized_a(db, tracked); 23 | if a > 8 { 24 | a 25 | } else { 26 | a + incr 27 | } 28 | } 29 | 30 | #[salsa::input] 31 | struct MyInput { 32 | field: u32, 33 | } 34 | 35 | #[salsa::tracked] 36 | struct MyTracked<'db> { 37 | field: u32, 38 | } 39 | 40 | #[test] 41 | fn cycle_memoized() { 42 | let mut db = salsa::DatabaseImpl::new(); 43 | let input = MyInput::new(&db, 2); 44 | assert_eq!(memoized(&db, input), 10); 45 | input.set_field(&mut db).to(3); 46 | assert_eq!(memoized(&db, input), 9); 47 | } 48 | -------------------------------------------------------------------------------- /tests/tracked_method_with_self_ty.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! Test that a `tracked` fn with `Self` in its signature or body on a `salsa::input` 4 | //! compiles and executes successfully. 5 | #![allow(warnings)] 6 | 7 | trait TrackedTrait { 8 | type Type; 9 | 10 | fn tracked_trait_fn(self, db: &dyn salsa::Database, ty: Self::Type) -> Self::Type; 11 | 12 | fn untracked_trait_fn(); 13 | } 14 | 15 | #[salsa::input] 16 | struct MyInput { 17 | field: u32, 18 | } 19 | 20 | #[salsa::tracked] 21 | impl MyInput { 22 | #[salsa::tracked] 23 | fn tracked_fn(self, db: &dyn salsa::Database, other: Self) -> u32 { 24 | self.field(db) + other.field(db) 25 | } 26 | } 27 | 28 | #[salsa::tracked] 29 | impl TrackedTrait for MyInput { 30 | type Type = u32; 31 | 32 | #[salsa::tracked] 33 | fn tracked_trait_fn(self, db: &dyn salsa::Database, ty: Self::Type) -> Self::Type { 34 | Self::untracked_trait_fn(); 35 | Self::tracked_fn(self, db, self) + ty 36 | } 37 | 38 | fn untracked_trait_fn() {} 39 | } 40 | 41 | #[test] 42 | fn execute() { 43 | let mut db = salsa::DatabaseImpl::new(); 44 | let object = MyInput::new(&mut db, 10); 45 | assert_eq!(object.tracked_trait_fn(&db, 1), 21); 46 | } 47 | -------------------------------------------------------------------------------- /tests/tracked_fn_no_eq.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | mod common; 4 | 5 | use common::LogDatabase; 6 | use expect_test::expect; 7 | use salsa::Setter as _; 8 | 9 | #[salsa::input] 10 | struct Input { 11 | number: i16, 12 | } 13 | 14 | #[salsa::tracked(no_eq)] 15 | fn abs_float(db: &dyn LogDatabase, input: Input) -> f32 { 16 | let number = input.number(db); 17 | 18 | db.push_log(format!("abs_float({number})")); 19 | number.abs() as f32 20 | } 21 | 22 | #[salsa::tracked] 23 | fn derived(db: &dyn LogDatabase, input: Input) -> u32 { 24 | let x = abs_float(db, input); 25 | db.push_log("derived".to_string()); 26 | 27 | x as u32 28 | } 29 | #[test] 30 | fn invoke() { 31 | let mut db = common::LoggerDatabase::default(); 32 | 33 | let input = Input::new(&db, 5); 34 | let x = derived(&db, input); 35 | 36 | assert_eq!(x, 5); 37 | 38 | input.set_number(&mut db).to(-5); 39 | 40 | // Derived should re-execute even the result of `abs_float` is the same. 41 | let x = derived(&db, input); 42 | assert_eq!(x, 5); 43 | 44 | db.assert_logs(expect![[r#" 45 | [ 46 | "abs_float(5)", 47 | "derived", 48 | "abs_float(-5)", 49 | "derived", 50 | ]"#]]); 51 | } 52 | -------------------------------------------------------------------------------- /tests/cycle_recovery_call_back_into_cycle.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! Calling back into the same cycle from your cycle recovery function _can_ work out, as long as 4 | //! the overall cycle still converges. 5 | 6 | mod common; 7 | use common::{DatabaseWithValue, ValueDatabase}; 8 | 9 | #[salsa::tracked] 10 | fn fallback_value(db: &dyn ValueDatabase) -> u32 { 11 | query(db) + db.get_value() 12 | } 13 | 14 | #[salsa::tracked(cycle_fn=cycle_fn, cycle_initial=cycle_initial)] 15 | fn query(db: &dyn ValueDatabase) -> u32 { 16 | let val = query(db); 17 | if val < 5 { 18 | val + 1 19 | } else { 20 | val 21 | } 22 | } 23 | 24 | fn cycle_initial(_db: &dyn ValueDatabase, _id: salsa::Id) -> u32 { 25 | 0 26 | } 27 | 28 | fn cycle_fn( 29 | db: &dyn ValueDatabase, 30 | _cycle: &salsa::Cycle, 31 | last_provisional_value: &u32, 32 | value: u32, 33 | ) -> u32 { 34 | if &value == last_provisional_value { 35 | value 36 | } else { 37 | fallback_value(db) 38 | } 39 | } 40 | 41 | #[test] 42 | fn converges() { 43 | let db = DatabaseWithValue::new(10); 44 | 45 | assert_eq!(query(&db), 10); 46 | } 47 | 48 | #[test] 49 | fn diverges() { 50 | let db = DatabaseWithValue::new(3); 51 | 52 | query(&db); 53 | } 54 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/rust 3 | { 4 | "name": "Rust", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/rust:1-1-bullseye", 7 | "features": { 8 | "ghcr.io/devcontainers-contrib/features/ripgrep:1": {} 9 | } 10 | 11 | // Use 'mounts' to make the cargo cache persistent in a Docker Volume. 12 | // "mounts": [ 13 | // { 14 | // "source": "devcontainer-cargo-cache-${devcontainerId}", 15 | // "target": "/usr/local/cargo", 16 | // "type": "volume" 17 | // } 18 | // ] 19 | 20 | // Features to add to the dev container. More info: https://containers.dev/features. 21 | // "features": {}, 22 | 23 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 24 | // "forwardPorts": [], 25 | 26 | // Use 'postCreateCommand' to run commands after the container is created. 27 | // "postCreateCommand": "rustc --version", 28 | 29 | // Configure tool-specific properties. 30 | // "customizations": {}, 31 | 32 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 33 | // "remoteUser": "root" 34 | } 35 | -------------------------------------------------------------------------------- /tests/parallel/cycle_panic.rs: -------------------------------------------------------------------------------- 1 | // Shuttle doesn't like panics inside of its runtime. 2 | #![cfg(not(feature = "shuttle"))] 3 | 4 | //! Test for panic in cycle recovery function, in cross-thread cycle. 5 | use crate::setup::{Knobs, KnobsDatabase}; 6 | 7 | #[salsa::tracked(cycle_fn=cycle_fn, cycle_initial=initial)] 8 | fn query_a(db: &dyn KnobsDatabase) -> u32 { 9 | db.signal(1); 10 | db.wait_for(2); 11 | query_b(db) 12 | } 13 | 14 | #[salsa::tracked(cycle_fn=cycle_fn, cycle_initial=initial)] 15 | fn query_b(db: &dyn KnobsDatabase) -> u32 { 16 | db.wait_for(1); 17 | db.signal(2); 18 | query_a(db) + 1 19 | } 20 | 21 | fn cycle_fn( 22 | _db: &dyn KnobsDatabase, 23 | _cycle: &salsa::Cycle, 24 | _last_provisional_value: &u32, 25 | _value: u32, 26 | ) -> u32 { 27 | panic!("cancel!") 28 | } 29 | 30 | fn initial(_db: &dyn KnobsDatabase, _id: salsa::Id) -> u32 { 31 | 0 32 | } 33 | 34 | #[test] 35 | fn execute() { 36 | let db = Knobs::default(); 37 | 38 | let db_t1 = db.clone(); 39 | let t1 = std::thread::spawn(move || query_a(&db_t1)); 40 | 41 | let db_t2 = db.clone(); 42 | let t2 = std::thread::spawn(move || query_b(&db_t2)); 43 | 44 | // The main thing here is that we don't deadlock. 45 | let (r1, r2) = (t1.join(), t2.join()); 46 | assert!(r1.is_err()); 47 | assert!(r2.is_err()); 48 | } 49 | -------------------------------------------------------------------------------- /tests/singleton.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! Basic Singleton struct test: 4 | //! 5 | //! Singleton structs are created only once. Subsequent `get`s and `new`s after creation return the same `Id`. 6 | 7 | use expect_test::expect; 8 | use salsa::Database as _; 9 | use test_log::test; 10 | 11 | #[salsa::input(singleton, debug)] 12 | struct MyInput { 13 | field: u32, 14 | id_field: u16, 15 | } 16 | 17 | #[test] 18 | fn basic() { 19 | let db = salsa::DatabaseImpl::new(); 20 | let input1 = MyInput::new(&db, 3, 4); 21 | let input2 = MyInput::get(&db); 22 | 23 | assert_eq!(input1, input2); 24 | 25 | let input3 = MyInput::try_get(&db); 26 | assert_eq!(Some(input1), input3); 27 | } 28 | 29 | #[test] 30 | #[should_panic] 31 | fn twice() { 32 | let db = salsa::DatabaseImpl::new(); 33 | let input1 = MyInput::new(&db, 3, 4); 34 | let input2 = MyInput::get(&db); 35 | 36 | assert_eq!(input1, input2); 37 | 38 | // should panic here 39 | _ = MyInput::new(&db, 3, 5); 40 | } 41 | 42 | #[test] 43 | fn debug() { 44 | salsa::DatabaseImpl::new().attach(|db| { 45 | let input = MyInput::new(db, 3, 4); 46 | let actual = format!("{input:?}"); 47 | let expected = expect!["MyInput { [salsa id]: Id(0), field: 3, id_field: 4 }"]; 48 | expected.assert_eq(&actual); 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /tests/tracked_fn_on_tracked_specify.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! Test that a `tracked` fn on a `salsa::input` 4 | //! compiles and executes successfully. 5 | #![allow(warnings)] 6 | 7 | #[salsa::input] 8 | struct MyInput { 9 | field: u32, 10 | } 11 | 12 | #[salsa::tracked] 13 | struct MyTracked<'db> { 14 | field: u32, 15 | } 16 | 17 | #[salsa::tracked] 18 | fn tracked_fn<'db>(db: &'db dyn salsa::Database, input: MyInput) -> MyTracked<'db> { 19 | let t = MyTracked::new(db, input.field(db) * 2); 20 | if input.field(db) != 0 { 21 | tracked_fn_extra::specify(db, t, 2222); 22 | } 23 | t 24 | } 25 | 26 | #[salsa::tracked(specify)] 27 | fn tracked_fn_extra<'db>(_db: &'db dyn salsa::Database, _input: MyTracked<'db>) -> u32 { 28 | 0 29 | } 30 | 31 | #[test] 32 | fn execute_when_specified() { 33 | let mut db = salsa::DatabaseImpl::new(); 34 | let input = MyInput::new(&db, 22); 35 | let tracked = tracked_fn(&db, input); 36 | assert_eq!(tracked.field(&db), 44); 37 | assert_eq!(tracked_fn_extra(&db, tracked), 2222); 38 | } 39 | 40 | #[test] 41 | fn execute_when_not_specified() { 42 | let mut db = salsa::DatabaseImpl::new(); 43 | let input = MyInput::new(&db, 0); 44 | let tracked = tracked_fn(&db, input); 45 | assert_eq!(tracked.field(&db), 0); 46 | assert_eq!(tracked_fn_extra(&db, tracked), 0); 47 | } 48 | -------------------------------------------------------------------------------- /src/accumulator/accumulated.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | use std::fmt::Debug; 3 | 4 | use crate::accumulator::Accumulator; 5 | 6 | #[derive(Clone, Debug)] 7 | pub(crate) struct Accumulated { 8 | values: Vec, 9 | } 10 | 11 | pub(crate) trait AnyAccumulated: Any + Send + Sync { 12 | fn as_dyn_any(&self) -> &dyn Any; 13 | fn as_dyn_any_mut(&mut self) -> &mut dyn Any; 14 | } 15 | 16 | impl Accumulated { 17 | pub fn push(&mut self, value: A) { 18 | self.values.push(value); 19 | } 20 | 21 | pub fn extend_with_accumulated<'slf>(&'slf self, values: &mut Vec<&'slf A>) { 22 | values.extend(&self.values); 23 | } 24 | } 25 | 26 | impl Default for Accumulated { 27 | fn default() -> Self { 28 | Self { 29 | values: Default::default(), 30 | } 31 | } 32 | } 33 | 34 | impl AnyAccumulated for Accumulated 35 | where 36 | A: Accumulator, 37 | { 38 | fn as_dyn_any(&self) -> &dyn Any { 39 | self 40 | } 41 | 42 | fn as_dyn_any_mut(&mut self) -> &mut dyn Any { 43 | self 44 | } 45 | } 46 | 47 | impl dyn AnyAccumulated { 48 | pub fn accumulate(&mut self, value: A) { 49 | self.as_dyn_any_mut() 50 | .downcast_mut::>() 51 | .unwrap() 52 | .push(value); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/function/lru.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroUsize; 2 | 3 | use crate::hash::FxLinkedHashSet; 4 | use crate::sync::Mutex; 5 | use crate::Id; 6 | 7 | pub(super) struct Lru { 8 | capacity: Option, 9 | set: Mutex>, 10 | } 11 | 12 | impl Lru { 13 | pub fn new(cap: usize) -> Self { 14 | Self { 15 | capacity: NonZeroUsize::new(cap), 16 | set: Mutex::default(), 17 | } 18 | } 19 | 20 | #[inline(always)] 21 | pub(super) fn record_use(&self, index: Id) { 22 | if self.capacity.is_some() { 23 | self.insert(index); 24 | } 25 | } 26 | 27 | #[inline(never)] 28 | fn insert(&self, index: Id) { 29 | let mut set = self.set.lock(); 30 | set.insert(index); 31 | } 32 | 33 | pub(super) fn set_capacity(&mut self, capacity: usize) { 34 | self.capacity = NonZeroUsize::new(capacity); 35 | if self.capacity.is_none() { 36 | self.set.get_mut().clear(); 37 | } 38 | } 39 | 40 | pub(super) fn for_each_evicted(&mut self, mut cb: impl FnMut(Id)) { 41 | let Some(cap) = self.capacity else { 42 | return; 43 | }; 44 | let set = self.set.get_mut(); 45 | while set.len() > cap.get() { 46 | if let Some(id) = set.pop_front() { 47 | cb(id); 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/compile-fail/tracked_struct_incompatibles.stderr: -------------------------------------------------------------------------------- 1 | error: `returns` option not allowed here 2 | --> tests/compile-fail/tracked_struct_incompatibles.rs:1:18 3 | | 4 | 1 | #[salsa::tracked(returns(ref))] 5 | | ^^^^^^^ 6 | 7 | error: `specify` option not allowed here 8 | --> tests/compile-fail/tracked_struct_incompatibles.rs:6:18 9 | | 10 | 6 | #[salsa::tracked(specify)] 11 | | ^^^^^^^ 12 | 13 | error: `no_eq` option not allowed here 14 | --> tests/compile-fail/tracked_struct_incompatibles.rs:11:18 15 | | 16 | 11 | #[salsa::tracked(no_eq)] 17 | | ^^^^^ 18 | 19 | error: `db` option not allowed here 20 | --> tests/compile-fail/tracked_struct_incompatibles.rs:16:18 21 | | 22 | 16 | #[salsa::tracked(db = Db)] 23 | | ^^ 24 | 25 | error: unrecognized option `recover_fn` 26 | --> tests/compile-fail/tracked_struct_incompatibles.rs:21:18 27 | | 28 | 21 | #[salsa::tracked(recover_fn = recover)] 29 | | ^^^^^^^^^^ 30 | 31 | error: `lru` option not allowed here 32 | --> tests/compile-fail/tracked_struct_incompatibles.rs:26:18 33 | | 34 | 26 | #[salsa::tracked(lru = 12)] 35 | | ^^^ 36 | 37 | error: `revisions` option not allowed here 38 | --> tests/compile-fail/tracked_struct_incompatibles.rs:31:18 39 | | 40 | 31 | #[salsa::tracked(revisions = 12)] 41 | | ^^^^^^^^^ 42 | -------------------------------------------------------------------------------- /tests/compile-fail/specify-does-not-work-if-the-key-is-a-salsa-input.stderr: -------------------------------------------------------------------------------- 1 | error[E0277]: the trait bound `MyInput: TrackedStructInDb` is not satisfied 2 | --> tests/compile-fail/specify-does-not-work-if-the-key-is-a-salsa-input.rs:15:1 3 | | 4 | 15 | #[salsa::tracked(specify)] 5 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound 6 | | 7 | help: the trait `TrackedStructInDb` is not implemented for `MyInput` 8 | --> tests/compile-fail/specify-does-not-work-if-the-key-is-a-salsa-input.rs:5:1 9 | | 10 | 5 | #[salsa::input] 11 | | ^^^^^^^^^^^^^^^ 12 | = help: the trait `TrackedStructInDb` is implemented for `MyTracked<'_>` 13 | note: required by a bound in `salsa::function::specify::>::specify_and_record` 14 | --> src/function/specify.rs 15 | | 16 | | pub fn specify_and_record<'db>(&'db self, db: &'db C::DbView, key: Id, value: C::Output<'db>) 17 | | ------------------ required by a bound in this associated function 18 | | where 19 | | C::Input<'db>: TrackedStructInDb, 20 | | ^^^^^^^^^^^^^^^^^ required by this bound in `salsa::function::specify::>::specify_and_record` 21 | = note: this error originates in the macro `salsa::plumbing::setup_tracked_fn` which comes from the expansion of the attribute macro `salsa::input` (in Nightly builds, run with -Z macro-backtrace for more info) 22 | -------------------------------------------------------------------------------- /tests/specify-only-works-if-the-key-is-created-in-the-current-query.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! Test that `specify` only works if the key is a tracked struct created in the current query. 4 | //! compilation succeeds but execution panics 5 | #![allow(warnings)] 6 | 7 | #[salsa::input] 8 | struct MyInput { 9 | field: u32, 10 | } 11 | 12 | #[salsa::tracked] 13 | struct MyTracked<'db> { 14 | field: u32, 15 | } 16 | 17 | #[salsa::tracked] 18 | fn tracked_struct_created_in_another_query<'db>( 19 | db: &'db dyn salsa::Database, 20 | input: MyInput, 21 | ) -> MyTracked<'db> { 22 | MyTracked::new(db, input.field(db) * 2) 23 | } 24 | 25 | #[salsa::tracked] 26 | fn tracked_fn<'db>(db: &'db dyn salsa::Database, input: MyInput) -> MyTracked<'db> { 27 | let t = tracked_struct_created_in_another_query(db, input); 28 | if input.field(db) != 0 { 29 | tracked_fn_extra::specify(db, t, 2222); 30 | } 31 | t 32 | } 33 | 34 | #[salsa::tracked(specify)] 35 | fn tracked_fn_extra<'db>(_db: &'db dyn salsa::Database, _input: MyTracked<'db>) -> u32 { 36 | 0 37 | } 38 | 39 | #[test] 40 | #[should_panic( 41 | expected = "can only use `specify` on salsa structs created during the current tracked fn" 42 | )] 43 | fn execute_when_specified() { 44 | let mut db = salsa::DatabaseImpl::new(); 45 | let input = MyInput::new(&db, 22); 46 | let tracked = tracked_fn(&db, input); 47 | } 48 | -------------------------------------------------------------------------------- /tests/compile-fail/specify-does-not-work-if-the-key-is-a-salsa-interned.stderr: -------------------------------------------------------------------------------- 1 | error[E0277]: the trait bound `MyInterned<'_>: TrackedStructInDb` is not satisfied 2 | --> tests/compile-fail/specify-does-not-work-if-the-key-is-a-salsa-interned.rs:15:1 3 | | 4 | 15 | #[salsa::tracked(specify)] 5 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound 6 | | 7 | help: the trait `TrackedStructInDb` is not implemented for `MyInterned<'_>` 8 | --> tests/compile-fail/specify-does-not-work-if-the-key-is-a-salsa-interned.rs:5:1 9 | | 10 | 5 | #[salsa::interned] 11 | | ^^^^^^^^^^^^^^^^^^ 12 | = help: the trait `TrackedStructInDb` is implemented for `MyTracked<'_>` 13 | note: required by a bound in `salsa::function::specify::>::specify_and_record` 14 | --> src/function/specify.rs 15 | | 16 | | pub fn specify_and_record<'db>(&'db self, db: &'db C::DbView, key: Id, value: C::Output<'db>) 17 | | ------------------ required by a bound in this associated function 18 | | where 19 | | C::Input<'db>: TrackedStructInDb, 20 | | ^^^^^^^^^^^^^^^^^ required by this bound in `salsa::function::specify::>::specify_and_record` 21 | = note: this error originates in the macro `salsa::plumbing::setup_tracked_fn` which comes from the expansion of the attribute macro `salsa::interned` (in Nightly builds, run with -Z macro-backtrace for more info) 22 | -------------------------------------------------------------------------------- /tests/compile-fail/tracked_fn_return_ref.stderr: -------------------------------------------------------------------------------- 1 | error[E0106]: missing lifetime specifier 2 | --> tests/compile-fail/tracked_fn_return_ref.rs:33:6 3 | | 4 | 33 | ) -> ContainsRef { 5 | | ^^^^^^^^^^^ expected named lifetime parameter 6 | | 7 | help: consider using the `'db` lifetime 8 | | 9 | 33 | ) -> ContainsRef<'db> { 10 | | +++++ 11 | 12 | error: lifetime may not live long enough 13 | --> tests/compile-fail/tracked_fn_return_ref.rs:15:67 14 | | 15 | 15 | fn tracked_fn_return_ref<'db>(db: &'db dyn Db, input: MyInput) -> &'db str { 16 | | --- lifetime `'db` defined here ^ requires that `'db` must outlive `'static` 17 | 18 | error: lifetime may not live long enough 19 | --> tests/compile-fail/tracked_fn_return_ref.rs:23:6 20 | | 21 | 20 | fn tracked_fn_return_struct_containing_ref<'db>( 22 | | --- lifetime `'db` defined here 23 | ... 24 | 23 | ) -> ContainsRef<'db> { 25 | | ^^^^^^^^^^^ requires that `'db` must outlive `'static` 26 | 27 | error: lifetime may not live long enough 28 | --> tests/compile-fail/tracked_fn_return_ref.rs:43:6 29 | | 30 | 40 | fn tracked_fn_return_struct_containing_ref_elided_explicit<'db>( 31 | | --- lifetime `'db` defined here 32 | ... 33 | 43 | ) -> ContainsRef<'_> { 34 | | ^^^^^^^^^^^ requires that `'db` must outlive `'static` 35 | -------------------------------------------------------------------------------- /src/nonce.rs: -------------------------------------------------------------------------------- 1 | use crate::sync::atomic::{AtomicU32, Ordering}; 2 | use std::marker::PhantomData; 3 | use std::num::NonZeroU32; 4 | 5 | /// A type to generate nonces. Store it in a static and each nonce it produces will be unique from other nonces. 6 | /// The type parameter `T` just serves to distinguish different kinds of nonces. 7 | pub(crate) struct NonceGenerator { 8 | value: AtomicU32, 9 | phantom: PhantomData, 10 | } 11 | 12 | /// A "nonce" is a value that gets created exactly once. 13 | /// We use it to mark the database storage so we can be sure we're seeing the same database. 14 | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] 15 | pub struct Nonce(NonZeroU32, PhantomData); 16 | 17 | impl NonceGenerator { 18 | pub(crate) const fn new() -> Self { 19 | Self { 20 | // start at 1 so we can detect rollover more easily 21 | value: AtomicU32::new(1), 22 | phantom: PhantomData, 23 | } 24 | } 25 | 26 | pub(crate) fn nonce(&self) -> Nonce { 27 | let value = self.value.fetch_add(1, Ordering::Relaxed); 28 | 29 | assert!(value != 0, "nonce rolled over"); 30 | 31 | Nonce(NonZeroU32::new(value).unwrap(), self.phantom) 32 | } 33 | } 34 | 35 | impl Nonce { 36 | pub(crate) fn into_u32(self) -> NonZeroU32 { 37 | self.0 38 | } 39 | 40 | pub(crate) fn from_u32(u32: NonZeroU32) -> Self { 41 | Self(u32, PhantomData) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /components/salsa-macros/src/debug.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | use std::process::{Command, Stdio}; 3 | use std::sync::OnceLock; 4 | 5 | use proc_macro2::TokenStream; 6 | 7 | static SALSA_DEBUG_MACRO: OnceLock> = OnceLock::new(); 8 | 9 | pub(crate) fn debug_enabled(input_name: impl ToString) -> bool { 10 | let Some(env_name) = SALSA_DEBUG_MACRO.get_or_init(|| std::env::var("SALSA_DEBUG_MACRO").ok()) 11 | else { 12 | return false; 13 | }; 14 | 15 | let input_name = input_name.to_string(); 16 | env_name == "*" || env_name == &input_name[..] 17 | } 18 | 19 | pub(crate) fn dump_tokens(input_name: impl ToString, tokens: TokenStream) -> TokenStream { 20 | if debug_enabled(input_name) { 21 | let token_string = tokens.to_string(); 22 | 23 | let _: Result<(), ()> = Command::new("rustfmt") 24 | .arg("--emit=stdout") 25 | .stdin(Stdio::piped()) 26 | .spawn() 27 | .and_then(|mut rustfmt| { 28 | rustfmt 29 | .stdin 30 | .take() 31 | .unwrap() 32 | .write_all(token_string.as_bytes())?; 33 | rustfmt.wait_with_output() 34 | }) 35 | .map(|output| eprintln!("{}", String::from_utf8_lossy(&output.stdout))) 36 | .or_else(|_| { 37 | eprintln!("{token_string}"); 38 | Ok(()) 39 | }); 40 | } 41 | 42 | tokens 43 | } 44 | -------------------------------------------------------------------------------- /tests/compile-fail/tracked_impl_incompatibles.rs: -------------------------------------------------------------------------------- 1 | #[salsa::tracked] 2 | struct MyTracked<'db> { 3 | field: u32, 4 | } 5 | 6 | #[salsa::tracked(returns(ref))] 7 | impl<'db> std::default::Default for MyTracked<'db> { 8 | fn default() -> Self {} 9 | } 10 | 11 | #[salsa::tracked(specify)] 12 | impl<'db> std::default::Default for MyTracked<'db> { 13 | fn default() -> Self {} 14 | } 15 | 16 | #[salsa::tracked(no_eq)] 17 | impl<'db> std::default::Default for MyTracked<'db> { 18 | fn default() -> Self {} 19 | } 20 | 21 | #[salsa::tracked(data = Data)] 22 | impl<'db> std::default::Default for MyTracked<'db> { 23 | fn default() -> Self {} 24 | } 25 | 26 | #[salsa::tracked(db = Db)] 27 | impl<'db> std::default::Default for MyTracked<'db> { 28 | fn default() -> Self {} 29 | } 30 | 31 | #[salsa::tracked(recover_fn = recover)] 32 | impl<'db> std::default::Default for MyTracked<'db> { 33 | fn default() -> Self {} 34 | } 35 | 36 | #[salsa::tracked(lru = 32)] 37 | impl<'db> std::default::Default for MyTracked<'db> { 38 | fn default() -> Self {} 39 | } 40 | 41 | #[salsa::tracked(revisions = 32)] 42 | impl<'db> std::default::Default for MyTracked<'db> { 43 | fn default() -> Self {} 44 | } 45 | 46 | #[salsa::tracked(constructor = Constructor)] 47 | impl<'db> std::default::Default for MyTracked<'db> { 48 | fn default() -> Self {} 49 | } 50 | 51 | #[salsa::tracked] 52 | impl<'db> std::default::Default for [MyTracked<'db>; 12] { 53 | fn default() -> Self {} 54 | } 55 | 56 | fn main() {} 57 | -------------------------------------------------------------------------------- /components/salsa-macro-rules/src/setup_tracked_assoc_fn_body.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! setup_tracked_assoc_fn_body { 3 | ( 4 | salsa_tracked_attr: #[$salsa_tracked_attr:meta], 5 | self_ty: $self_ty:ty, 6 | db_lt: $($db_lt:lifetime)?, 7 | db: $db:ident, 8 | db_ty: ($($db_ty:tt)*), 9 | input_ids: [$($input_id:ident),*], 10 | input_tys: [$($input_ty:ty),*], 11 | output_ty: $output_ty:ty, 12 | inner_fn_name: $inner_fn_name:ident, 13 | inner_fn: $inner_fn:item, 14 | 15 | // Annoyingly macro-rules hygiene does not extend to items defined in the macro. 16 | // We have the procedural macro generate names for those items that are 17 | // not used elsewhere in the user's code. 18 | unused_names: [ 19 | $InnerTrait:ident, 20 | ] 21 | ) => { 22 | { 23 | trait $InnerTrait<$($db_lt)?> { 24 | fn $inner_fn_name(db: $($db_ty)*, $($input_id: $input_ty),*) -> $output_ty; 25 | } 26 | 27 | impl<$($db_lt)?> $InnerTrait<$($db_lt)?> for $self_ty { 28 | $inner_fn 29 | } 30 | 31 | #[$salsa_tracked_attr] 32 | fn $inner_fn_name<$($db_lt)?>(db: $($db_ty)*, $($input_id: $input_ty),*) -> $output_ty { 33 | <$self_ty as $InnerTrait>::$inner_fn_name(db, $($input_id),*) 34 | } 35 | 36 | $inner_fn_name($db, $($input_id),*) 37 | } 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/database_impl.rs: -------------------------------------------------------------------------------- 1 | use tracing::Level; 2 | 3 | use crate::storage::HasStorage; 4 | use crate::{Database, Storage}; 5 | 6 | /// Default database implementation that you can use if you don't 7 | /// require any custom user data. 8 | #[derive(Clone)] 9 | pub struct DatabaseImpl { 10 | storage: Storage, 11 | } 12 | 13 | impl Default for DatabaseImpl { 14 | fn default() -> Self { 15 | Self { 16 | // Default behavior: tracing debug log the event. 17 | storage: Storage::new(if tracing::enabled!(Level::DEBUG) { 18 | Some(Box::new(|event| { 19 | crate::tracing::debug!("salsa_event({:?})", event) 20 | })) 21 | } else { 22 | None 23 | }), 24 | } 25 | } 26 | } 27 | 28 | impl DatabaseImpl { 29 | /// Create a new database; equivalent to `Self::default`. 30 | pub fn new() -> Self { 31 | Self::default() 32 | } 33 | 34 | pub fn storage(&self) -> &Storage { 35 | &self.storage 36 | } 37 | } 38 | 39 | impl Database for DatabaseImpl {} 40 | 41 | // SAFETY: The `storage` and `storage_mut` fields return a reference to the same storage field owned by `self`. 42 | unsafe impl HasStorage for DatabaseImpl { 43 | #[inline(always)] 44 | fn storage(&self) -> &Storage { 45 | &self.storage 46 | } 47 | 48 | #[inline(always)] 49 | fn storage_mut(&mut self) -> &mut Storage { 50 | &mut self.storage 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/tracked_fn_read_own_specify.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | use expect_test::expect; 4 | mod common; 5 | use common::LogDatabase; 6 | use salsa::Database; 7 | 8 | #[salsa::input(debug)] 9 | struct MyInput { 10 | field: u32, 11 | } 12 | 13 | #[salsa::tracked(debug)] 14 | struct MyTracked<'db> { 15 | field: u32, 16 | } 17 | 18 | #[salsa::tracked] 19 | fn tracked_fn(db: &dyn LogDatabase, input: MyInput) -> u32 { 20 | db.push_log(format!("tracked_fn({input:?})")); 21 | let t = MyTracked::new(db, input.field(db) * 2); 22 | tracked_fn_extra::specify(db, t, 2222); 23 | tracked_fn_extra(db, t) 24 | } 25 | 26 | #[salsa::tracked(specify)] 27 | fn tracked_fn_extra<'db>(db: &'db dyn LogDatabase, input: MyTracked<'db>) -> u32 { 28 | db.push_log(format!("tracked_fn_extra({input:?})")); 29 | 0 30 | } 31 | 32 | #[test] 33 | fn execute() { 34 | let mut db = common::LoggerDatabase::default(); 35 | let input = MyInput::new(&db, 22); 36 | assert_eq!(tracked_fn(&db, input), 2222); 37 | db.assert_logs(expect![[r#" 38 | [ 39 | "tracked_fn(MyInput { [salsa id]: Id(0), field: 22 })", 40 | ]"#]]); 41 | 42 | // A "synthetic write" causes the system to act *as though* some 43 | // input of durability `durability` has changed. 44 | db.synthetic_write(salsa::Durability::LOW); 45 | 46 | // Re-run the query on the original input. Nothing re-executes! 47 | assert_eq!(tracked_fn(&db, input), 2222); 48 | db.assert_logs(expect!["[]"]); 49 | } 50 | -------------------------------------------------------------------------------- /tests/accumulate-chain.rs: -------------------------------------------------------------------------------- 1 | #![cfg(all(feature = "inventory", feature = "accumulator"))] 2 | 3 | //! Test that when having nested tracked functions 4 | //! we don't drop any values when accumulating. 5 | 6 | mod common; 7 | 8 | use expect_test::expect; 9 | use salsa::{Accumulator, Database, DatabaseImpl}; 10 | use test_log::test; 11 | 12 | #[salsa::accumulator] 13 | #[derive(Debug)] 14 | struct Log(#[allow(dead_code)] String); 15 | 16 | #[salsa::tracked] 17 | fn push_logs(db: &dyn Database) { 18 | push_a_logs(db); 19 | } 20 | 21 | #[salsa::tracked] 22 | fn push_a_logs(db: &dyn Database) { 23 | Log("log a".to_string()).accumulate(db); 24 | push_b_logs(db); 25 | } 26 | 27 | #[salsa::tracked] 28 | fn push_b_logs(db: &dyn Database) { 29 | // No logs 30 | push_c_logs(db); 31 | } 32 | 33 | #[salsa::tracked] 34 | fn push_c_logs(db: &dyn Database) { 35 | // No logs 36 | push_d_logs(db); 37 | } 38 | 39 | #[salsa::tracked] 40 | fn push_d_logs(db: &dyn Database) { 41 | Log("log d".to_string()).accumulate(db); 42 | } 43 | 44 | #[test] 45 | fn accumulate_chain() { 46 | DatabaseImpl::new().attach(|db| { 47 | let logs = push_logs::accumulated::(db); 48 | // Check that we get all the logs. 49 | expect![[r#" 50 | [ 51 | Log( 52 | "log a", 53 | ), 54 | Log( 55 | "log d", 56 | ), 57 | ]"#]] 58 | .assert_eq(&format!("{logs:#?}")); 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /tests/durability.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! Tests that code using the builder's durability methods compiles. 4 | 5 | use salsa::{Database, Durability, Setter}; 6 | use test_log::test; 7 | 8 | #[salsa::input] 9 | struct N { 10 | value: u32, 11 | } 12 | 13 | #[salsa::tracked] 14 | fn add3(db: &dyn Database, a: N, b: N, c: N) -> u32 { 15 | add(db, a, b) + c.value(db) 16 | } 17 | 18 | #[salsa::tracked] 19 | fn add(db: &dyn Database, a: N, b: N) -> u32 { 20 | a.value(db) + b.value(db) 21 | } 22 | 23 | #[test] 24 | fn durable_to_less_durable() { 25 | let mut db = salsa::DatabaseImpl::new(); 26 | 27 | let a = N::builder(11).value_durability(Durability::HIGH).new(&db); 28 | let b = N::builder(22).value_durability(Durability::HIGH).new(&db); 29 | let c = N::builder(33).value_durability(Durability::HIGH).new(&db); 30 | 31 | // Here, `add3` invokes `add(a, b)`, which yields 33. 32 | assert_eq!(add3(&db, a, b, c), 66); 33 | 34 | a.set_value(&mut db).with_durability(Durability::LOW).to(11); 35 | 36 | // Here, `add3` invokes `add`, which *still* yields 33, but which 37 | // is no longer of high durability. Since value didn't change, we might 38 | // preserve `add3` unchanged, not noticing that it is no longer 39 | // of high durability. 40 | 41 | assert_eq!(add3(&db, a, b, c), 66); 42 | 43 | // In that case, we would not get the correct result here, when 44 | // 'a' changes *again*. 45 | 46 | a.set_value(&mut db).to(22); 47 | 48 | assert_eq!(add3(&db, a, b, c), 77); 49 | } 50 | -------------------------------------------------------------------------------- /components/salsa-macro-rules/src/setup_tracked_method_body.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! setup_tracked_method_body { 3 | ( 4 | salsa_tracked_attr: #[$salsa_tracked_attr:meta], 5 | self: $self:ident, 6 | self_ty: $self_ty:ty, 7 | db_lt: $($db_lt:lifetime)?, 8 | db: $db:ident, 9 | db_ty: ($($db_ty:tt)*), 10 | input_ids: [$($input_id:ident),*], 11 | input_tys: [$($input_ty:ty),*], 12 | output_ty: $output_ty:ty, 13 | inner_fn_name: $inner_fn_name:ident, 14 | inner_fn: $inner_fn:item, 15 | 16 | // Annoyingly macro-rules hygiene does not extend to items defined in the macro. 17 | // We have the procedural macro generate names for those items that are 18 | // not used elsewhere in the user's code. 19 | unused_names: [ 20 | $InnerTrait:ident, 21 | ] 22 | ) => { 23 | { 24 | trait $InnerTrait<$($db_lt)?> { 25 | fn $inner_fn_name($self, db: $($db_ty)*, $($input_id: $input_ty),*) -> $output_ty; 26 | } 27 | 28 | impl<$($db_lt)?> $InnerTrait<$($db_lt)?> for $self_ty { 29 | $inner_fn 30 | } 31 | 32 | #[$salsa_tracked_attr] 33 | fn $inner_fn_name<$($db_lt)?>(db: $($db_ty)*, this: $self_ty, $($input_id: $input_ty),*) -> $output_ty { 34 | <$self_ty as $InnerTrait>::$inner_fn_name(this, db, $($input_id),*) 35 | } 36 | 37 | $inner_fn_name($db, $self, $($input_id),*) 38 | } 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /tests/cycle_fallback_immediate.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! It is possible to omit the `cycle_fn`, only specifying `cycle_result` in which case 4 | //! an immediate fallback value is used as the cycle handling opposed to doing a fixpoint resolution. 5 | 6 | use std::sync::atomic::{AtomicI32, Ordering}; 7 | 8 | #[salsa::tracked(cycle_result=cycle_result)] 9 | fn one_o_one(db: &dyn salsa::Database) -> u32 { 10 | let val = one_o_one(db); 11 | val + 1 12 | } 13 | 14 | fn cycle_result(_db: &dyn salsa::Database, _id: salsa::Id) -> u32 { 15 | 100 16 | } 17 | 18 | #[test_log::test] 19 | fn simple() { 20 | let db = salsa::DatabaseImpl::default(); 21 | 22 | assert_eq!(one_o_one(&db), 100); 23 | } 24 | 25 | #[salsa::tracked(cycle_result=two_queries_cycle_result)] 26 | fn two_queries1(db: &dyn salsa::Database) -> i32 { 27 | two_queries2(db); 28 | 0 29 | } 30 | 31 | #[salsa::tracked] 32 | fn two_queries2(db: &dyn salsa::Database) -> i32 { 33 | two_queries1(db); 34 | // This is horribly against Salsa's rules, but we want to test that 35 | // the value from within the cycle is not considered, and this is 36 | // the only way I found. 37 | static CALLS_COUNT: AtomicI32 = AtomicI32::new(0); 38 | CALLS_COUNT.fetch_add(1, Ordering::Relaxed) 39 | } 40 | 41 | fn two_queries_cycle_result(_db: &dyn salsa::Database, _id: salsa::Id) -> i32 { 42 | 1 43 | } 44 | 45 | #[test] 46 | fn two_queries() { 47 | let db = salsa::DatabaseImpl::default(); 48 | 49 | assert_eq!(two_queries1(&db), 1); 50 | assert_eq!(two_queries2(&db), 1); 51 | } 52 | -------------------------------------------------------------------------------- /tests/tracked_struct.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | mod common; 4 | 5 | use salsa::{Database, Setter}; 6 | 7 | #[salsa::tracked] 8 | struct Tracked<'db> { 9 | untracked_1: usize, 10 | 11 | untracked_2: usize, 12 | } 13 | 14 | #[salsa::input] 15 | struct MyInput { 16 | field1: usize, 17 | field2: usize, 18 | } 19 | 20 | #[salsa::tracked] 21 | fn intermediate(db: &dyn salsa::Database, input: MyInput) -> Tracked<'_> { 22 | Tracked::new(db, input.field1(db), input.field2(db)) 23 | } 24 | 25 | #[salsa::tracked] 26 | fn accumulate(db: &dyn salsa::Database, input: MyInput) -> (usize, usize) { 27 | let tracked = intermediate(db, input); 28 | let one = read_tracked_1(db, tracked); 29 | let two = read_tracked_2(db, tracked); 30 | 31 | (one, two) 32 | } 33 | 34 | #[salsa::tracked] 35 | fn read_tracked_1<'db>(db: &'db dyn Database, tracked: Tracked<'db>) -> usize { 36 | tracked.untracked_1(db) 37 | } 38 | 39 | #[salsa::tracked] 40 | fn read_tracked_2<'db>(db: &'db dyn Database, tracked: Tracked<'db>) -> usize { 41 | tracked.untracked_2(db) 42 | } 43 | 44 | #[test] 45 | fn execute() { 46 | let mut db = salsa::DatabaseImpl::default(); 47 | let input = MyInput::new(&db, 1, 1); 48 | 49 | assert_eq!(accumulate(&db, input), (1, 1)); 50 | 51 | // Should only re-execute `read_tracked_1`. 52 | input.set_field1(&mut db).to(2); 53 | assert_eq!(accumulate(&db, input), (2, 1)); 54 | 55 | // Should only re-execute `read_tracked_2`. 56 | input.set_field2(&mut db).to(2); 57 | assert_eq!(accumulate(&db, input), (2, 2)); 58 | } 59 | -------------------------------------------------------------------------------- /tests/compile-fail/tracked_struct_not_update.stderr: -------------------------------------------------------------------------------- 1 | error[E0599]: the function or associated item `maybe_update` exists for struct `UpdateDispatch`, but its trait bounds were not satisfied 2 | --> tests/compile-fail/tracked_struct_not_update.rs:1:1 3 | | 4 | 1 | #[salsa::tracked] 5 | | ^^^^^^^^^^^^^^^^^ function or associated item cannot be called on `UpdateDispatch` due to unsatisfied trait bounds 6 | ... 7 | 7 | struct NotUpdate; 8 | | ---------------- doesn't satisfy `NotUpdate: PartialEq` or `NotUpdate: Update` 9 | | 10 | ::: src/update.rs 11 | | 12 | | pub struct Dispatch(PhantomData); 13 | | ---------------------- doesn't satisfy `_: UpdateFallback` 14 | | 15 | note: if you're trying to build a new `UpdateDispatch`, consider using `UpdateDispatch::::new` which returns `UpdateDispatch<_>` 16 | --> src/update.rs 17 | | 18 | | pub fn new() -> Self { 19 | | ^^^^^^^^^^^^^^^^^^^^ 20 | = note: the following trait bounds were not satisfied: 21 | `NotUpdate: Update` 22 | `NotUpdate: PartialEq` 23 | which is required by `UpdateDispatch: UpdateFallback` 24 | note: the trait `Update` must be implemented 25 | --> src/update.rs 26 | | 27 | | pub unsafe trait Update { 28 | | ^^^^^^^^^^^^^^^^^^^^^^^ 29 | = note: this error originates in the attribute macro `salsa::tracked` (in Nightly builds, run with -Z macro-backtrace for more info) 30 | help: consider annotating `NotUpdate` with `#[derive(PartialEq)]` 31 | | 32 | 7 + #[derive(PartialEq)] 33 | 8 | struct NotUpdate; 34 | | 35 | -------------------------------------------------------------------------------- /components/salsa-macros/src/fn_util.rs: -------------------------------------------------------------------------------- 1 | use crate::hygiene::Hygiene; 2 | use crate::xform::ChangeLt; 3 | 4 | /// Returns a vector of ids representing the function arguments. 5 | /// Prefers to reuse the names given by the user, if possible. 6 | pub fn input_ids(hygiene: &Hygiene, sig: &syn::Signature, skip: usize) -> Vec { 7 | sig.inputs 8 | .iter() 9 | .skip(skip) 10 | .zip(0..) 11 | .map(|(input, index)| { 12 | if let syn::FnArg::Typed(typed) = input { 13 | if let syn::Pat::Ident(ident) = &*typed.pat { 14 | return ident.ident.clone(); 15 | } 16 | } 17 | 18 | hygiene.ident(format!("input{index}")) 19 | }) 20 | .collect() 21 | } 22 | 23 | pub fn input_tys(sig: &syn::Signature, skip: usize) -> syn::Result> { 24 | sig.inputs 25 | .iter() 26 | .skip(skip) 27 | .map(|input| { 28 | if let syn::FnArg::Typed(typed) = input { 29 | Ok(&*typed.ty) 30 | } else { 31 | Err(syn::Error::new_spanned(input, "unexpected receiver")) 32 | } 33 | }) 34 | .collect() 35 | } 36 | 37 | pub fn output_ty(db_lt: Option<&syn::Lifetime>, sig: &syn::Signature) -> syn::Result { 38 | match &sig.output { 39 | syn::ReturnType::Default => Ok(parse_quote!(())), 40 | syn::ReturnType::Type(_, ty) => match db_lt { 41 | Some(db_lt) => Ok(ChangeLt::elided_to(db_lt).in_type(ty)), 42 | None => Ok(syn::Type::clone(ty)), 43 | }, 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/parallel/signal.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | use super::sync::{Condvar, Mutex}; 4 | 5 | #[derive(Default)] 6 | pub(crate) struct Signal { 7 | value: Mutex, 8 | cond_var: Condvar, 9 | } 10 | 11 | impl Signal { 12 | pub(crate) fn signal(&self, stage: usize) { 13 | // When running with shuttle we want to explore as many possible 14 | // executions, so we avoid signals entirely. 15 | #[cfg(not(feature = "shuttle"))] 16 | { 17 | // This check avoids acquiring the lock for things that will 18 | // clearly be a no-op. Not *necessary* but helps to ensure we 19 | // are more likely to encounter weird race conditions; 20 | // otherwise calls to `sum` will tend to be unnecessarily 21 | // synchronous. 22 | if stage > 0 { 23 | let mut v = self.value.lock().unwrap(); 24 | if stage > *v { 25 | *v = stage; 26 | self.cond_var.notify_all(); 27 | } 28 | } 29 | } 30 | } 31 | 32 | /// Waits until the given condition is true; the fn is invoked 33 | /// with the current stage. 34 | pub(crate) fn wait_for(&self, stage: usize) { 35 | #[cfg(not(feature = "shuttle"))] 36 | { 37 | // As above, avoid lock if clearly a no-op. 38 | if stage > 0 { 39 | let mut v = self.value.lock().unwrap(); 40 | while *v < stage { 41 | v = self.cond_var.wait(v).unwrap(); 42 | } 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/compile-fail/invalid_persist_options.rs: -------------------------------------------------------------------------------- 1 | #[salsa::input(persist)] 2 | struct Input { 3 | text: String, 4 | } 5 | 6 | #[salsa::input(persist())] 7 | struct Input2 { 8 | text: String, 9 | } 10 | 11 | #[salsa::input(persist(serialize = serde::Serialize::serialize))] 12 | struct Input3 { 13 | text: String, 14 | } 15 | 16 | #[salsa::input(persist(deserialize = serde::Deserialize::deserialize))] 17 | struct Input4 { 18 | text: String, 19 | } 20 | 21 | #[salsa::input(persist(serialize = serde::Serialize::serialize, deserialize = serde::Deserialize::deserialize))] 22 | struct Input5 { 23 | text: String, 24 | } 25 | 26 | #[salsa::input(persist(serialize = serde::Serialize::serialize, serialize = serde::Serialize::serialize))] 27 | struct InvalidInput { 28 | text: String, 29 | } 30 | 31 | #[salsa::input(persist(deserialize = serde::Deserialize::deserialize, deserialize = serde::Deserialize::deserialize))] 32 | struct InvalidInput2 { 33 | text: String, 34 | } 35 | 36 | #[salsa::input(persist(not_an_option = std::convert::identity))] 37 | struct InvalidInput3 { 38 | text: String, 39 | } 40 | 41 | #[salsa::tracked(persist)] 42 | fn tracked_fn(db: &dyn salsa::Database, input: Input) -> String { 43 | input.text(db) 44 | } 45 | 46 | #[salsa::tracked(persist())] 47 | fn tracked_fn2(db: &dyn salsa::Database, input: Input) -> String { 48 | input.text(db) 49 | } 50 | 51 | #[salsa::tracked(persist(serialize = serde::Serialize::serialize, deserialize = serde::Deserialize::deserialize))] 52 | fn invalid_tracked_fn(db: &dyn salsa::Database, input: Input) -> String { 53 | input.text(db) 54 | } 55 | 56 | fn main() {} 57 | -------------------------------------------------------------------------------- /book/src/tuning.md: -------------------------------------------------------------------------------- 1 | # Tuning Salsa 2 | 3 | ## LRU Cache 4 | 5 | You can specify an LRU cache size for any non-input query: 6 | 7 | ```rs 8 | let lru_capacity: usize = 128; 9 | base_db::ParseQuery.in_db_mut(self).set_lru_capacity(lru_capacity); 10 | ``` 11 | 12 | The default is `0`, which disables LRU-caching entirely. 13 | 14 | Note that there is no garbage collection for keys and 15 | results of old queries, so LRU caches are currently the 16 | only knob available for avoiding unbounded memory usage 17 | for long-running apps built on Salsa. 18 | 19 | ## Intern Queries 20 | 21 | Intern queries can make key lookup cheaper, save memory, and 22 | avoid the need for [`Arc`](https://doc.rust-lang.org/std/sync/struct.Arc.html). 23 | 24 | Interning is especially useful for queries that involve nested, 25 | tree-like data structures. 26 | 27 | See: 28 | 29 | - The [`compiler` example](https://github.com/salsa-rs/salsa/blob/master/examples/compiler/main.rs), 30 | which uses interning. 31 | 32 | ## Cancellation 33 | 34 | Queries that are no longer needed due to concurrent writes or changes in dependencies are cancelled 35 | by Salsa. Each access of an intermediate query is a potential cancellation point. Cancellation is 36 | implemented via panicking, and Salsa internals are intended to be panic-safe. 37 | 38 | If you have a query that contains a long loop which does not execute any intermediate queries, 39 | salsa won't be able to cancel it automatically. You may wish to check for cancellation yourself 40 | by invoking `db.unwind_if_cancelled()`. 41 | 42 | For more details on cancellation, see the tests for cancellation behavior in the Salsa repo. 43 | -------------------------------------------------------------------------------- /tests/cycle_input_different_cycle_head.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! Tests that the durability correctly propagates 4 | //! to all cycle heads. 5 | 6 | use salsa::Setter as _; 7 | 8 | #[test_log::test] 9 | fn low_durability_cycle_enter_from_different_head() { 10 | let mut db = MyDbImpl::default(); 11 | // Start with 0, the same as returned by cycle initial 12 | let input = Input::builder(0).new(&db); 13 | db.input = Some(input); 14 | 15 | assert_eq!(query_a(&db), 0); // Prime the Db 16 | 17 | input.set_value(&mut db).to(10); 18 | 19 | assert_eq!(query_b(&db), 10); 20 | } 21 | 22 | #[salsa::input] 23 | struct Input { 24 | value: u32, 25 | } 26 | 27 | #[salsa::db] 28 | trait MyDb: salsa::Database { 29 | fn input(&self) -> Input; 30 | } 31 | 32 | #[salsa::db] 33 | #[derive(Clone, Default)] 34 | struct MyDbImpl { 35 | storage: salsa::Storage, 36 | input: Option, 37 | } 38 | 39 | #[salsa::db] 40 | impl salsa::Database for MyDbImpl {} 41 | 42 | #[salsa::db] 43 | impl MyDb for MyDbImpl { 44 | fn input(&self) -> Input { 45 | self.input.unwrap() 46 | } 47 | } 48 | 49 | #[salsa::tracked(cycle_initial=cycle_initial)] 50 | fn query_a(db: &dyn MyDb) -> u32 { 51 | query_b(db); 52 | db.input().value(db) 53 | } 54 | 55 | fn cycle_initial(_db: &dyn MyDb, _id: salsa::Id) -> u32 { 56 | 0 57 | } 58 | 59 | #[salsa::interned] 60 | struct Interned { 61 | value: u32, 62 | } 63 | 64 | #[salsa::tracked(cycle_initial=cycle_initial)] 65 | fn query_b<'db>(db: &'db dyn MyDb) -> u32 { 66 | query_c(db) 67 | } 68 | 69 | #[salsa::tracked] 70 | fn query_c(db: &dyn MyDb) -> u32 { 71 | query_a(db) 72 | } 73 | -------------------------------------------------------------------------------- /tests/tracked_method.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! Test that a `tracked` fn on a `salsa::input` 4 | //! compiles and executes successfully. 5 | #![allow(warnings)] 6 | 7 | use common::LogDatabase as _; 8 | use expect_test::expect; 9 | 10 | mod common; 11 | 12 | trait TrackedTrait { 13 | fn tracked_trait_fn(self, db: &dyn salsa::Database) -> u32; 14 | } 15 | 16 | #[salsa::input] 17 | struct MyInput { 18 | field: u32, 19 | } 20 | 21 | #[salsa::tracked] 22 | impl MyInput { 23 | #[salsa::tracked] 24 | fn tracked_fn(self, db: &dyn salsa::Database) -> u32 { 25 | self.field(db) * 2 26 | } 27 | 28 | #[salsa::tracked(returns(ref))] 29 | fn tracked_fn_ref(self, db: &dyn salsa::Database) -> u32 { 30 | self.field(db) * 3 31 | } 32 | } 33 | 34 | #[salsa::tracked] 35 | impl TrackedTrait for MyInput { 36 | #[salsa::tracked] 37 | fn tracked_trait_fn(self, db: &dyn salsa::Database) -> u32 { 38 | self.field(db) * 4 39 | } 40 | } 41 | 42 | #[test] 43 | fn execute() { 44 | let mut db = salsa::DatabaseImpl::new(); 45 | let object = MyInput::new(&mut db, 22); 46 | // assert_eq!(object.tracked_fn(&db), 44); 47 | // assert_eq!(*object.tracked_fn_ref(&db), 66); 48 | assert_eq!(object.tracked_trait_fn(&db), 88); 49 | } 50 | 51 | #[test] 52 | fn debug_name() { 53 | let mut db = common::ExecuteValidateLoggerDatabase::default(); 54 | let object = MyInput::new(&mut db, 22); 55 | 56 | assert_eq!(object.tracked_trait_fn(&db), 88); 57 | db.assert_logs(expect![[r#" 58 | [ 59 | "salsa_event(WillExecute { database_key: MyInput::tracked_trait_fn_(Id(0)) })", 60 | ]"#]]); 61 | } 62 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # Frequently asked questions 2 | 3 | ## Why is it called salsa? 4 | 5 | I like salsa! Don't you?! Well, ok, there's a bit more to it. The 6 | underlying algorithm for figuring out which bits of code need to be 7 | re-executed after any given change is based on the algorithm used in 8 | rustc. Michael Woerister and I first described the rustc algorithm in 9 | terms of two colors, red and green, and hence we called it the 10 | "red-green algorithm". This made me think of the New Mexico State 11 | Question --- ["Red or green?"][nm] --- which refers to chile 12 | (salsa). Although this version no longer uses colors (we borrowed 13 | revision counters from Glimmer, instead), I still like the name. 14 | 15 | [nm]: https://www.sos.state.nm.us/about-new-mexico/state-question/ 16 | 17 | ## What is the relationship between salsa and an Entity-Component System (ECS)? 18 | 19 | You may have noticed that Salsa "feels" a lot like an ECS in some 20 | ways. That's true -- Salsa's queries are a bit like *components* (and 21 | the keys to the queries are a bit like *entities*). But there is one 22 | big difference: **ECS is -- at its heart -- a mutable system**. You 23 | can get or set a component of some entity whenever you like. In 24 | contrast, salsa's queries **define "derived values" via pure 25 | computations**. 26 | 27 | Partly as a consequence, ECS doesn't handle incremental updates for 28 | you. When you update some component of some entity, you have to ensure 29 | that other entities' components are updated appropriately. 30 | 31 | Finally, ECS offers interesting metadata and "aspect-like" facilities, 32 | such as iterating over all entities that share certain components. 33 | Salsa has no analogue to that. 34 | 35 | -------------------------------------------------------------------------------- /tests/tracked_struct_with_interned_query.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | mod common; 4 | 5 | use salsa::Setter; 6 | 7 | #[salsa::input] 8 | struct MyInput { 9 | value: usize, 10 | } 11 | 12 | #[salsa::tracked] 13 | struct Tracked<'db> { 14 | value: String, 15 | } 16 | 17 | #[salsa::tracked] 18 | fn query_tracked(db: &dyn salsa::Database, input: MyInput) -> Tracked<'_> { 19 | Tracked::new(db, format!("{value}", value = input.value(db))) 20 | } 21 | 22 | #[salsa::tracked] 23 | fn join<'db>(db: &'db dyn salsa::Database, tracked: Tracked<'db>, with: String) -> String { 24 | format!("{}{}", tracked.value(db), with) 25 | } 26 | 27 | #[test] 28 | fn execute() { 29 | let mut db = salsa::DatabaseImpl::default(); 30 | let input = MyInput::new(&db, 1); 31 | 32 | let tracked = query_tracked(&db, input); 33 | let joined = join(&db, tracked, "world".to_string()); 34 | 35 | assert_eq!(joined, "1world"); 36 | 37 | // Create a new revision: This puts the tracked struct created in revision 0 38 | // into the free list. 39 | input.set_value(&mut db).to(2); 40 | 41 | let tracked = query_tracked(&db, input); 42 | let joined = join(&db, tracked, "world".to_string()); 43 | 44 | assert_eq!(joined, "2world"); 45 | 46 | // Create a new revision: The tracked struct created in revision 0 is now 47 | // reused, including its id. The argument to `join` will hash and compare 48 | // equal to the argument used in revision 0 but the return value should be 49 | // 3world and not 1world. 50 | input.set_value(&mut db).to(3); 51 | 52 | let tracked = query_tracked(&db, input); 53 | let joined = join(&db, tracked, "world".to_string()); 54 | 55 | assert_eq!(joined, "3world"); 56 | } 57 | -------------------------------------------------------------------------------- /src/input/singleton.rs: -------------------------------------------------------------------------------- 1 | use crate::sync::atomic::{AtomicU64, Ordering}; 2 | use crate::Id; 3 | 4 | mod sealed { 5 | pub trait Sealed {} 6 | } 7 | 8 | pub trait SingletonChoice: sealed::Sealed + Default { 9 | fn with_scope(&self, cb: impl FnOnce() -> Id) -> Id; 10 | fn index(&self) -> Option; 11 | } 12 | 13 | pub struct Singleton { 14 | index: AtomicU64, 15 | } 16 | impl sealed::Sealed for Singleton {} 17 | impl SingletonChoice for Singleton { 18 | fn with_scope(&self, cb: impl FnOnce() -> Id) -> Id { 19 | if self.index.load(Ordering::Acquire) != 0 { 20 | panic!("singleton struct may not be duplicated"); 21 | } 22 | let id = cb(); 23 | if self 24 | .index 25 | .compare_exchange(0, id.as_bits(), Ordering::AcqRel, Ordering::Acquire) 26 | .is_err() 27 | { 28 | panic!("singleton struct may not be duplicated"); 29 | } 30 | id 31 | } 32 | 33 | fn index(&self) -> Option { 34 | match self.index.load(Ordering::Acquire) { 35 | 0 => None, 36 | 37 | // SAFETY: Our u64 is derived from an ID and thus safe to convert back. 38 | id => Some(unsafe { Id::from_bits_unchecked(id) }), 39 | } 40 | } 41 | } 42 | 43 | impl Default for Singleton { 44 | fn default() -> Self { 45 | Self { 46 | index: AtomicU64::new(0), 47 | } 48 | } 49 | } 50 | #[derive(Default)] 51 | pub struct NotSingleton; 52 | impl sealed::Sealed for NotSingleton {} 53 | impl SingletonChoice for NotSingleton { 54 | fn with_scope(&self, cb: impl FnOnce() -> Id) -> Id { 55 | cb() 56 | } 57 | fn index(&self) -> Option { 58 | None 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/accumulate-execution-order.rs: -------------------------------------------------------------------------------- 1 | #![cfg(all(feature = "inventory", feature = "accumulator"))] 2 | 3 | //! Demonstrates that accumulation is done in the order 4 | //! in which things were originally executed. 5 | 6 | mod common; 7 | 8 | use expect_test::expect; 9 | use salsa::{Accumulator, Database}; 10 | use test_log::test; 11 | 12 | #[salsa::accumulator] 13 | #[derive(Debug)] 14 | struct Log(#[allow(dead_code)] String); 15 | 16 | #[salsa::tracked] 17 | fn push_logs(db: &dyn Database) { 18 | push_a_logs(db); 19 | } 20 | 21 | #[salsa::tracked] 22 | fn push_a_logs(db: &dyn Database) { 23 | Log("log a".to_string()).accumulate(db); 24 | push_b_logs(db); 25 | push_c_logs(db); 26 | push_d_logs(db); 27 | } 28 | 29 | #[salsa::tracked] 30 | fn push_b_logs(db: &dyn Database) { 31 | Log("log b".to_string()).accumulate(db); 32 | push_d_logs(db); 33 | } 34 | 35 | #[salsa::tracked] 36 | fn push_c_logs(db: &dyn Database) { 37 | Log("log c".to_string()).accumulate(db); 38 | } 39 | 40 | #[salsa::tracked] 41 | fn push_d_logs(db: &dyn Database) { 42 | Log("log d".to_string()).accumulate(db); 43 | } 44 | 45 | #[test] 46 | fn accumulate_execution_order() { 47 | salsa::DatabaseImpl::new().attach(|db| { 48 | let logs = push_logs::accumulated::(db); 49 | // Check that we get logs in execution order 50 | expect![[r#" 51 | [ 52 | Log( 53 | "log a", 54 | ), 55 | Log( 56 | "log b", 57 | ), 58 | Log( 59 | "log d", 60 | ), 61 | Log( 62 | "log c", 63 | ), 64 | ]"#]] 65 | .assert_eq(&format!("{logs:#?}")); 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /tests/compile-fail/interned_struct_incompatibles.stderr: -------------------------------------------------------------------------------- 1 | error: `returns` option not allowed here 2 | --> tests/compile-fail/interned_struct_incompatibles.rs:1:19 3 | | 4 | 1 | #[salsa::interned(returns(ref))] 5 | | ^^^^^^^ 6 | 7 | error: `specify` option not allowed here 8 | --> tests/compile-fail/interned_struct_incompatibles.rs:6:19 9 | | 10 | 6 | #[salsa::interned(specify)] 11 | | ^^^^^^^ 12 | 13 | error: `no_eq` option not allowed here 14 | --> tests/compile-fail/interned_struct_incompatibles.rs:11:19 15 | | 16 | 11 | #[salsa::interned(no_eq)] 17 | | ^^^^^ 18 | 19 | error: `db` option not allowed here 20 | --> tests/compile-fail/interned_struct_incompatibles.rs:16:19 21 | | 22 | 16 | #[salsa::interned(db = Db)] 23 | | ^^ 24 | 25 | error: unrecognized option `recover_fn` 26 | --> tests/compile-fail/interned_struct_incompatibles.rs:21:19 27 | | 28 | 21 | #[salsa::interned(recover_fn = recover)] 29 | | ^^^^^^^^^^ 30 | 31 | error: `lru` option not allowed here 32 | --> tests/compile-fail/interned_struct_incompatibles.rs:26:19 33 | | 34 | 26 | #[salsa::interned(lru = 12)] 35 | | ^^^ 36 | 37 | error: `#[tracked]` cannot be used with `#[salsa::interned]` 38 | --> tests/compile-fail/interned_struct_incompatibles.rs:33:5 39 | | 40 | 33 | / #[tracked] 41 | 34 | | field: u32, 42 | | |______________^ 43 | 44 | error: cannot find attribute `tracked` in this scope 45 | --> tests/compile-fail/interned_struct_incompatibles.rs:33:7 46 | | 47 | 33 | #[tracked] 48 | | ^^^^^^^ 49 | | 50 | help: consider importing one of these attribute macros 51 | | 52 | 1 + use salsa::tracked; 53 | | 54 | 1 + use salsa_macros::tracked; 55 | | 56 | -------------------------------------------------------------------------------- /book/src/plumbing/cycles.md: -------------------------------------------------------------------------------- 1 | # Cycles 2 | 3 | ## Cross-thread blocking 4 | 5 | The interface for blocking across threads now works as follows: 6 | 7 | * When one thread `T1` wishes to block on a query `Q` being executed by another thread `T2`, it invokes `Runtime::try_block_on`. This will check for cycles. Assuming no cycle is detected, it will block `T1` until `T2` has completed with `Q`. At that point, `T1` reawakens. However, we don't know the result of executing `Q`, so `T1` now has to "retry". Typically, this will result in successfully reading the cached value. 8 | * While `T1` is blocking, the runtime moves its query stack (a `Vec`) into the shared dependency graph data structure. When `T1` reawakens, it recovers ownership of its query stack before returning from `try_block_on`. 9 | 10 | ## Cycle detection 11 | 12 | When a thread `T1` attempts to execute a query `Q`, it will try to load the value for `Q` from the memoization tables. If it finds an `InProgress` marker, that indicates that `Q` is currently being computed. This indicates a potential cycle. `T1` will then try to block on the query `Q`: 13 | 14 | * If `Q` is also being computed by `T1`, then there is a cycle. 15 | * Otherwise, if `Q` is being computed by some other thread `T2`, we have to check whether `T2` is (transitively) blocked on `T1`. If so, there is a cycle. 16 | 17 | These two cases are handled internally by the `Runtime::try_block_on` function. Detecting the intra-thread cycle case is easy; to detect cross-thread cycles, the runtime maintains a dependency DAG between threads (identified by `RuntimeId`). Before adding an edge `T1 -> T2` (i.e., `T1` is blocked waiting for `T2`) into the DAG, it checks whether a path exists from `T2` to `T1`. If so, we have a cycle and the edge cannot be added (then the DAG would not longer be acyclic). 18 | -------------------------------------------------------------------------------- /src/cancelled.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::panic::{self, UnwindSafe}; 3 | 4 | /// A panic payload indicating that execution of a salsa query was cancelled. 5 | /// 6 | /// This can occur for a few reasons: 7 | /// * 8 | /// * 9 | /// * 10 | #[derive(Debug)] 11 | #[non_exhaustive] 12 | pub enum Cancelled { 13 | /// The query was operating on revision R, but there is a pending write to move to revision R+1. 14 | #[non_exhaustive] 15 | PendingWrite, 16 | 17 | /// The query was blocked on another thread, and that thread panicked. 18 | #[non_exhaustive] 19 | PropagatedPanic, 20 | } 21 | 22 | impl Cancelled { 23 | #[cold] 24 | pub(crate) fn throw(self) -> ! { 25 | // We use resume and not panic here to avoid running the panic 26 | // hook (that is, to avoid collecting and printing backtrace). 27 | panic::resume_unwind(Box::new(self)); 28 | } 29 | 30 | /// Runs `f`, and catches any salsa cancellation. 31 | pub fn catch(f: F) -> Result 32 | where 33 | F: FnOnce() -> T + UnwindSafe, 34 | { 35 | match panic::catch_unwind(f) { 36 | Ok(t) => Ok(t), 37 | Err(payload) => match payload.downcast() { 38 | Ok(cancelled) => Err(*cancelled), 39 | Err(payload) => panic::resume_unwind(payload), 40 | }, 41 | } 42 | } 43 | } 44 | 45 | impl std::fmt::Display for Cancelled { 46 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 47 | let why = match self { 48 | Cancelled::PendingWrite => "pending write", 49 | Cancelled::PropagatedPanic => "propagated panic", 50 | }; 51 | f.write_str("cancelled because of ")?; 52 | f.write_str(why) 53 | } 54 | } 55 | 56 | impl std::error::Error for Cancelled {} 57 | -------------------------------------------------------------------------------- /src/tracing.rs: -------------------------------------------------------------------------------- 1 | //! Wrappers around `tracing` macros that avoid inlining debug machinery into the hot path, 2 | //! as tracing events are typically only enabled for debugging purposes. 3 | 4 | macro_rules! trace { 5 | ($($x:tt)*) => { 6 | crate::tracing::event!(TRACE, $($x)*) 7 | }; 8 | } 9 | 10 | macro_rules! warn_event { 11 | ($($x:tt)*) => { 12 | crate::tracing::event!(WARN, $($x)*) 13 | }; 14 | } 15 | 16 | macro_rules! info { 17 | ($($x:tt)*) => { 18 | crate::tracing::event!(INFO, $($x)*) 19 | }; 20 | } 21 | 22 | macro_rules! debug { 23 | ($($x:tt)*) => { 24 | crate::tracing::event!(DEBUG, $($x)*) 25 | }; 26 | } 27 | 28 | macro_rules! debug_span { 29 | ($($x:tt)*) => { 30 | crate::tracing::span!(DEBUG, $($x)*) 31 | }; 32 | } 33 | 34 | #[expect(unused_macros)] 35 | macro_rules! info_span { 36 | ($($x:tt)*) => { 37 | crate::tracing::span!(INFO, $($x)*) 38 | }; 39 | } 40 | 41 | macro_rules! event { 42 | ($level:ident, $($x:tt)*) => {{ 43 | let event = { 44 | #[cold] #[inline(never)] || { ::tracing::event!(::tracing::Level::$level, $($x)*) } 45 | }; 46 | 47 | if ::tracing::enabled!(::tracing::Level::$level) { 48 | event(); 49 | } 50 | }}; 51 | } 52 | 53 | macro_rules! span { 54 | ($level:ident, $($x:tt)*) => {{ 55 | let span = { 56 | #[cold] #[inline(never)] || { ::tracing::span!(::tracing::Level::$level, $($x)*) } 57 | }; 58 | 59 | if ::tracing::enabled!(::tracing::Level::$level) { 60 | span() 61 | } else { 62 | ::tracing::Span::none() 63 | } 64 | }}; 65 | } 66 | 67 | #[expect(unused_imports)] 68 | pub(crate) use {debug, debug_span, event, info, info_span, span, trace, warn_event as warn}; 69 | -------------------------------------------------------------------------------- /tests/override_new_get_set.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! Test that the `constructor` macro overrides 4 | //! the `new` method's name and `get` and `set` 5 | //! change the name of the getter and setter of the fields. 6 | #![allow(warnings)] 7 | 8 | use std::fmt::Display; 9 | 10 | use salsa::Setter; 11 | 12 | #[salsa::db] 13 | trait Db: salsa::Database {} 14 | 15 | #[salsa::input(constructor = from_string)] 16 | struct MyInput { 17 | #[get(text)] 18 | #[set(set_text)] 19 | field: String, 20 | } 21 | 22 | impl MyInput { 23 | pub fn new(db: &mut dyn Db, s: impl Display) -> MyInput { 24 | MyInput::from_string(db, s.to_string()) 25 | } 26 | 27 | pub fn field(self, db: &dyn Db) -> String { 28 | self.text(db) 29 | } 30 | 31 | pub fn set_field(self, db: &mut dyn Db, id: String) { 32 | self.set_text(db).to(id); 33 | } 34 | } 35 | 36 | #[salsa::interned(constructor = from_string)] 37 | struct MyInterned<'db> { 38 | #[get(text)] 39 | #[returns(ref)] 40 | field: String, 41 | } 42 | 43 | impl<'db> MyInterned<'db> { 44 | pub fn new(db: &'db dyn Db, s: impl Display) -> MyInterned<'db> { 45 | MyInterned::from_string(db, s.to_string()) 46 | } 47 | 48 | pub fn field(self, db: &'db dyn Db) -> &str { 49 | &self.text(db) 50 | } 51 | } 52 | 53 | #[salsa::tracked(constructor = from_string)] 54 | struct MyTracked<'db> { 55 | #[get(text)] 56 | field: String, 57 | } 58 | 59 | impl<'db> MyTracked<'db> { 60 | pub fn new(db: &'db dyn Db, s: impl Display) -> MyTracked<'db> { 61 | MyTracked::from_string(db, s.to_string()) 62 | } 63 | 64 | pub fn field(self, db: &'db dyn Db) -> String { 65 | self.text(db) 66 | } 67 | } 68 | 69 | #[test] 70 | fn execute() { 71 | salsa::DatabaseImpl::new(); 72 | } 73 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release-plz 2 | 3 | permissions: 4 | pull-requests: write 5 | contents: write 6 | 7 | on: 8 | push: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | # Release unpublished packages. 14 | release-plz-release: 15 | if: ${{ github.repository_owner == 'salsa-rs' }} 16 | name: Release-plz release 17 | runs-on: ubuntu-latest 18 | permissions: 19 | contents: write 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 0 25 | - name: Install Rust toolchain 26 | uses: dtolnay/rust-toolchain@stable 27 | - name: Run release-plz 28 | uses: release-plz/action@v0.5 29 | with: 30 | command: release 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 34 | 35 | # Create a PR with the new versions and changelog, preparing the next release. 36 | release-plz-pr: 37 | if: ${{ github.repository_owner == 'salsa-rs' }} 38 | name: Release-plz PR 39 | runs-on: ubuntu-latest 40 | permissions: 41 | contents: write 42 | pull-requests: write 43 | concurrency: 44 | group: release-plz-${{ github.ref }} 45 | cancel-in-progress: false 46 | steps: 47 | - name: Checkout repository 48 | uses: actions/checkout@v4 49 | with: 50 | fetch-depth: 0 51 | - name: Install Rust toolchain 52 | uses: dtolnay/rust-toolchain@stable 53 | - name: Run release-plz 54 | uses: release-plz/action@v0.5 55 | with: 56 | command: release-pr 57 | env: 58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 60 | -------------------------------------------------------------------------------- /book/src/tutorial/accumulators.md: -------------------------------------------------------------------------------- 1 | # Defining the parser: reporting errors 2 | 3 | The last interesting case in the parser is how to handle a parse error. 4 | Because Salsa functions are memoized and may not execute, they should not have side-effects, 5 | so we don't just want to call `eprintln!`. 6 | If we did so, the error would only be reported the first time the function was called, but not 7 | on subsequent calls in the situation where the simply returns its memoized value. 8 | 9 | Salsa defines a mechanism for managing this called an **accumulator**. 10 | In our case, we define an accumulator struct called `Diagnostics` in the `ir` module: 11 | 12 | ```rust 13 | {{#include ../../../examples/calc/ir.rs:diagnostic}} 14 | ``` 15 | 16 | Accumulator structs are always newtype structs with a single field, in this case of type `Diagnostic`. 17 | Memoized functions can _push_ `Diagnostic` values onto the accumulator. 18 | Later, you can invoke a method to find all the values that were pushed by the memoized functions 19 | or any functions that they called 20 | (e.g., we could get the set of `Diagnostic` values produced by the `parse_statements` function). 21 | 22 | The `Parser::report_error` method contains an example of pushing a diagnostic: 23 | 24 | ```rust 25 | {{#include ../../../examples/calc/parser.rs:report_error}} 26 | ``` 27 | 28 | To get the set of diagnostics produced by `parse_errors`, or any other memoized function, 29 | we invoke the associated `accumulated` function: 30 | 31 | ```rust 32 | let accumulated: Vec = 33 | parse_statements::accumulated::(db); 34 | // ----------- 35 | // Use turbofish to specify 36 | // the diagnostics type. 37 | ``` 38 | 39 | `accumulated` takes the database `db` as argument and returns a `Vec`. 40 | -------------------------------------------------------------------------------- /tests/tracked_method_on_tracked_struct.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | use salsa::Database; 4 | 5 | #[derive(Debug, PartialEq, Eq, Hash)] 6 | pub struct Item {} 7 | 8 | #[salsa::input] 9 | pub struct Input { 10 | name: String, 11 | } 12 | 13 | #[salsa::tracked] 14 | impl Input { 15 | #[salsa::tracked] 16 | pub fn source_tree(self, db: &dyn Database) -> SourceTree<'_> { 17 | SourceTree::new(db, self.name(db).clone()) 18 | } 19 | } 20 | 21 | #[salsa::tracked] 22 | pub struct SourceTree<'db> { 23 | name: String, 24 | } 25 | 26 | #[salsa::tracked] 27 | impl<'db1> SourceTree<'db1> { 28 | #[salsa::tracked(returns(ref))] 29 | pub fn inherent_item_name(self, db: &'db1 dyn Database) -> String { 30 | self.name(db) 31 | } 32 | } 33 | 34 | trait ItemName<'db1> { 35 | fn trait_item_name(self, db: &'db1 dyn Database) -> &'db1 String; 36 | } 37 | 38 | #[salsa::tracked] 39 | impl<'db1> ItemName<'db1> for SourceTree<'db1> { 40 | #[salsa::tracked(returns(ref))] 41 | fn trait_item_name(self, db: &'db1 dyn Database) -> String { 42 | self.name(db) 43 | } 44 | } 45 | 46 | #[test] 47 | fn test_inherent() { 48 | salsa::DatabaseImpl::new().attach(|db| { 49 | let input = Input::new(db, "foo".to_string()); 50 | let source_tree = input.source_tree(db); 51 | expect_test::expect![[r#" 52 | "foo" 53 | "#]] 54 | .assert_debug_eq(source_tree.inherent_item_name(db)); 55 | }) 56 | } 57 | 58 | #[test] 59 | fn test_trait() { 60 | salsa::DatabaseImpl::new().attach(|db| { 61 | let input = Input::new(db, "foo".to_string()); 62 | let source_tree = input.source_tree(db); 63 | expect_test::expect![[r#" 64 | "foo" 65 | "#]] 66 | .assert_debug_eq(source_tree.trait_item_name(db)); 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /tests/tracked_with_struct_db.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! Test that a setting a field on a `#[salsa::input]` 4 | //! overwrites and returns the old value. 5 | 6 | use salsa::{Database, DatabaseImpl, Update}; 7 | use test_log::test; 8 | 9 | #[salsa::input(debug)] 10 | struct MyInput { 11 | field: String, 12 | } 13 | 14 | #[salsa::tracked(debug)] 15 | struct MyTracked<'db> { 16 | #[tracked] 17 | data: MyInput, 18 | #[tracked] 19 | next: MyList<'db>, 20 | } 21 | 22 | #[derive(PartialEq, Eq, Clone, Debug, Update)] 23 | enum MyList<'db> { 24 | None, 25 | Next(MyTracked<'db>), 26 | } 27 | 28 | #[salsa::tracked] 29 | fn create_tracked_list(db: &dyn Database, input: MyInput) -> MyTracked<'_> { 30 | let t0 = MyTracked::new(db, input, MyList::None); 31 | let t1 = MyTracked::new(db, input, MyList::Next(t0)); 32 | t1 33 | } 34 | 35 | #[test] 36 | fn execute() { 37 | DatabaseImpl::new().attach(|db| { 38 | let input = MyInput::new(db, "foo".to_string()); 39 | let t0: MyTracked = create_tracked_list(db, input); 40 | let t1 = create_tracked_list(db, input); 41 | expect_test::expect![[r#" 42 | MyTracked { 43 | [salsa id]: Id(401), 44 | data: MyInput { 45 | [salsa id]: Id(0), 46 | field: "foo", 47 | }, 48 | next: Next( 49 | MyTracked { 50 | [salsa id]: Id(400), 51 | data: MyInput { 52 | [salsa id]: Id(0), 53 | field: "foo", 54 | }, 55 | next: None, 56 | }, 57 | ), 58 | } 59 | "#]] 60 | .assert_debug_eq(&t0); 61 | assert_eq!(t0, t1); 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /tests/compile-fail/invalid_update_with.stderr: -------------------------------------------------------------------------------- 1 | error[E0308]: mismatched types 2 | --> tests/compile-fail/invalid_update_with.rs:3:26 3 | | 4 | 3 | #[update(unsafe(with(my_wrong_update)))] 5 | | ---- ^^^^^^^^^^^^^^^ incorrect number of function parameters 6 | | | 7 | | expected due to this 8 | | 9 | = note: expected fn pointer `unsafe fn(*mut i32, i32) -> bool` 10 | found fn item `fn() -> () {my_wrong_update}` 11 | 12 | error[E0308]: mismatched types 13 | --> tests/compile-fail/invalid_update_with.rs:5:26 14 | | 15 | 5 | #[update(unsafe(with(my_wrong_update2)))] 16 | | ---- ^^^^^^^^^^^^^^^^ expected fn pointer, found fn item 17 | | | 18 | | expected due to this 19 | | 20 | = note: expected fn pointer `unsafe fn(*mut i32, i32) -> bool` 21 | found fn item `fn((), ()) -> bool {my_wrong_update2}` 22 | 23 | error[E0308]: mismatched types 24 | --> tests/compile-fail/invalid_update_with.rs:7:26 25 | | 26 | 7 | #[update(unsafe(with(my_wrong_update3)))] 27 | | ---- ^^^^^^^^^^^^^^^^ expected fn pointer, found fn item 28 | | | 29 | | expected due to this 30 | | 31 | = note: expected fn pointer `unsafe fn(*mut i32, i32) -> bool` 32 | found fn item `fn(*mut i32, i32) -> () {my_wrong_update3}` 33 | 34 | error[E0308]: mismatched types 35 | --> tests/compile-fail/invalid_update_with.rs:9:26 36 | | 37 | 9 | #[update(unsafe(with(true)))] 38 | | ---- ^^^^ expected fn pointer, found `bool` 39 | | | 40 | | expected due to this 41 | | 42 | = note: expected fn pointer `unsafe fn(*mut &'static str, &'static str) -> bool` 43 | found type `bool` 44 | -------------------------------------------------------------------------------- /tests/derive_update.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! Test that the `Update` derive works as expected 4 | 5 | #[derive(salsa::Update)] 6 | struct MyInput { 7 | field: &'static str, 8 | } 9 | 10 | #[derive(salsa::Update)] 11 | struct MyInput2 { 12 | #[update(unsafe(with(custom_update)))] 13 | field: &'static str, 14 | #[update(unsafe(with(|dest, data| { *dest = data; true })))] 15 | field2: &'static str, 16 | } 17 | 18 | unsafe fn custom_update(dest: *mut &'static str, _data: &'static str) -> bool { 19 | unsafe { *dest = "ill-behaved for testing purposes" }; 20 | true 21 | } 22 | 23 | #[test] 24 | fn derived() { 25 | let mut m = MyInput { field: "foo" }; 26 | assert_eq!(m.field, "foo"); 27 | assert!(unsafe { salsa::Update::maybe_update(&mut m, MyInput { field: "bar" }) }); 28 | assert_eq!(m.field, "bar"); 29 | assert!(!unsafe { salsa::Update::maybe_update(&mut m, MyInput { field: "bar" }) }); 30 | assert_eq!(m.field, "bar"); 31 | } 32 | 33 | #[test] 34 | fn derived_with() { 35 | let mut m = MyInput2 { 36 | field: "foo", 37 | field2: "foo", 38 | }; 39 | assert_eq!(m.field, "foo"); 40 | assert_eq!(m.field2, "foo"); 41 | assert!(unsafe { 42 | salsa::Update::maybe_update( 43 | &mut m, 44 | MyInput2 { 45 | field: "bar", 46 | field2: "bar", 47 | }, 48 | ) 49 | }); 50 | assert_eq!(m.field, "ill-behaved for testing purposes"); 51 | assert_eq!(m.field2, "bar"); 52 | assert!(unsafe { 53 | salsa::Update::maybe_update( 54 | &mut m, 55 | MyInput2 { 56 | field: "ill-behaved for testing purposes", 57 | field2: "foo", 58 | }, 59 | ) 60 | }); 61 | assert_eq!(m.field, "ill-behaved for testing purposes"); 62 | assert_eq!(m.field2, "foo"); 63 | } 64 | -------------------------------------------------------------------------------- /tests/compile-fail/input_struct_incompatibles.stderr: -------------------------------------------------------------------------------- 1 | error: `returns` option not allowed here 2 | --> tests/compile-fail/input_struct_incompatibles.rs:1:16 3 | | 4 | 1 | #[salsa::input(returns(ref))] 5 | | ^^^^^^^ 6 | 7 | error: `specify` option not allowed here 8 | --> tests/compile-fail/input_struct_incompatibles.rs:4:16 9 | | 10 | 4 | #[salsa::input(specify)] 11 | | ^^^^^^^ 12 | 13 | error: `no_eq` option not allowed here 14 | --> tests/compile-fail/input_struct_incompatibles.rs:7:16 15 | | 16 | 7 | #[salsa::input(no_eq)] 17 | | ^^^^^ 18 | 19 | error: `db` option not allowed here 20 | --> tests/compile-fail/input_struct_incompatibles.rs:10:16 21 | | 22 | 10 | #[salsa::input(db = Db)] 23 | | ^^ 24 | 25 | error: unrecognized option `recover_fn` 26 | --> tests/compile-fail/input_struct_incompatibles.rs:13:16 27 | | 28 | 13 | #[salsa::input(recover_fn = recover)] 29 | | ^^^^^^^^^^ 30 | 31 | error: `lru` option not allowed here 32 | --> tests/compile-fail/input_struct_incompatibles.rs:16:16 33 | | 34 | 16 | #[salsa::input(lru =12)] 35 | | ^^^ 36 | 37 | error: `revisions` option not allowed here 38 | --> tests/compile-fail/input_struct_incompatibles.rs:19:16 39 | | 40 | 19 | #[salsa::input(revisions = 12)] 41 | | ^^^^^^^^^ 42 | 43 | error: `#[tracked]` cannot be used with `#[salsa::input]` 44 | --> tests/compile-fail/input_struct_incompatibles.rs:24:5 45 | | 46 | 24 | / #[tracked] 47 | 25 | | field: u32, 48 | | |______________^ 49 | 50 | error: cannot find attribute `tracked` in this scope 51 | --> tests/compile-fail/input_struct_incompatibles.rs:24:7 52 | | 53 | 24 | #[tracked] 54 | | ^^^^^^^ 55 | | 56 | help: consider importing one of these attribute macros 57 | | 58 | 1 + use salsa::tracked; 59 | | 60 | 1 + use salsa_macros::tracked; 61 | | 62 | -------------------------------------------------------------------------------- /tests/tracked_struct_mixed_tracked_fields.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | mod common; 4 | 5 | use salsa::{Database, Setter}; 6 | 7 | // A tracked struct with mixed tracked and untracked fields to ensure 8 | // the correct field indices are used when tracking dependencies. 9 | #[salsa::tracked] 10 | struct Tracked<'db> { 11 | untracked_1: usize, 12 | 13 | #[tracked] 14 | tracked_1: usize, 15 | 16 | untracked_2: usize, 17 | 18 | untracked_3: usize, 19 | 20 | #[tracked] 21 | tracked_2: usize, 22 | 23 | untracked_4: usize, 24 | } 25 | 26 | #[salsa::input] 27 | struct MyInput { 28 | field1: usize, 29 | field2: usize, 30 | } 31 | 32 | #[salsa::tracked] 33 | fn intermediate(db: &dyn salsa::Database, input: MyInput) -> Tracked<'_> { 34 | Tracked::new(db, 0, input.field1(db), 0, 0, input.field2(db), 0) 35 | } 36 | 37 | #[salsa::tracked] 38 | fn accumulate(db: &dyn salsa::Database, input: MyInput) -> (usize, usize) { 39 | let tracked = intermediate(db, input); 40 | let one = read_tracked_1(db, tracked); 41 | let two = read_tracked_2(db, tracked); 42 | 43 | (one, two) 44 | } 45 | 46 | #[salsa::tracked] 47 | fn read_tracked_1<'db>(db: &'db dyn Database, tracked: Tracked<'db>) -> usize { 48 | tracked.tracked_1(db) 49 | } 50 | 51 | #[salsa::tracked] 52 | fn read_tracked_2<'db>(db: &'db dyn Database, tracked: Tracked<'db>) -> usize { 53 | tracked.tracked_2(db) 54 | } 55 | 56 | #[test] 57 | fn execute() { 58 | let mut db = salsa::DatabaseImpl::default(); 59 | let input = MyInput::new(&db, 1, 1); 60 | 61 | assert_eq!(accumulate(&db, input), (1, 1)); 62 | 63 | // Should only re-execute `read_tracked_1`. 64 | input.set_field1(&mut db).to(2); 65 | assert_eq!(accumulate(&db, input), (2, 1)); 66 | 67 | // Should only re-execute `read_tracked_2`. 68 | input.set_field2(&mut db).to(2); 69 | assert_eq!(accumulate(&db, input), (2, 2)); 70 | } 71 | -------------------------------------------------------------------------------- /tests/accumulate-dag.rs: -------------------------------------------------------------------------------- 1 | #![cfg(all(feature = "inventory", feature = "accumulator"))] 2 | 3 | mod common; 4 | 5 | use expect_test::expect; 6 | use salsa::{Accumulator, Database}; 7 | use test_log::test; 8 | 9 | #[salsa::input(debug)] 10 | struct MyInput { 11 | field_a: u32, 12 | field_b: u32, 13 | } 14 | 15 | #[salsa::accumulator] 16 | #[derive(Debug)] 17 | struct Log(#[allow(dead_code)] String); 18 | 19 | #[salsa::tracked] 20 | fn push_logs(db: &dyn Database, input: MyInput) { 21 | push_a_logs(db, input); 22 | push_b_logs(db, input); 23 | } 24 | 25 | #[salsa::tracked] 26 | fn push_a_logs(db: &dyn Database, input: MyInput) { 27 | let count = input.field_a(db); 28 | for i in 0..count { 29 | Log(format!("log_a({i} of {count})")).accumulate(db); 30 | } 31 | } 32 | 33 | #[salsa::tracked] 34 | fn push_b_logs(db: &dyn Database, input: MyInput) { 35 | // Note that b calls a 36 | push_a_logs(db, input); 37 | let count = input.field_b(db); 38 | for i in 0..count { 39 | Log(format!("log_b({i} of {count})")).accumulate(db); 40 | } 41 | } 42 | 43 | #[test] 44 | fn accumulate_a_called_twice() { 45 | salsa::DatabaseImpl::new().attach(|db| { 46 | let input = MyInput::new(db, 2, 3); 47 | let logs = push_logs::accumulated::(db, input); 48 | // Check that we don't see logs from `a` appearing twice in the input. 49 | expect![[r#" 50 | [ 51 | Log( 52 | "log_a(0 of 2)", 53 | ), 54 | Log( 55 | "log_a(1 of 2)", 56 | ), 57 | Log( 58 | "log_b(0 of 3)", 59 | ), 60 | Log( 61 | "log_b(1 of 3)", 62 | ), 63 | Log( 64 | "log_b(2 of 3)", 65 | ), 66 | ]"#]] 67 | .assert_eq(&format!("{logs:#?}")); 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /book/src/common_patterns/on_demand_inputs.md: -------------------------------------------------------------------------------- 1 | # On-Demand (Lazy) Inputs 2 | 3 | Salsa inputs work best if you can easily provide all of the inputs upfront. 4 | However sometimes the set of inputs is not known beforehand. 5 | 6 | A typical example is reading files from disk. 7 | While it is possible to eagerly scan a particular directory and create an in-memory file tree as salsa input structs, a more straight-forward approach is to read the files lazily. 8 | That is, when a query requests the text of a file for the first time: 9 | 10 | 1. Read the file from disk and cache it. 11 | 2. Setup a file-system watcher for this path. 12 | 3. Update the cached file when the watcher sends a change notification. 13 | 14 | This is possible to achieve in salsa, by caching the inputs in your database structs and adding a method to the database trait to retrieve them out of this cache. 15 | 16 | A complete, runnable file-watching example can be found in [the lazy-input example](https://github.com/salsa-rs/salsa/tree/master/examples/lazy-input). 17 | 18 | The setup looks roughly like this: 19 | 20 | ```rust,ignore 21 | {{#include ../../../examples/lazy-input/main.rs:db}} 22 | ``` 23 | 24 | - We declare a method on the `Db` trait that gives us a `File` input on-demand (it only requires a `&dyn Db` not a `&mut dyn Db`). 25 | - There should only be one input struct per file, so we implement that method using a cache (`DashMap` is like a `RwLock`). 26 | 27 | The driving code that's doing the top-level queries is then in charge of updating the file contents when a file-change notification arrives. 28 | It does this by updating the Salsa input in the same way that you would update any other input. 29 | 30 | Here we implement a simple driving loop, that recompiles the code whenever a file changes. 31 | You can use the logs to check that only the queries that could have changed are re-evaluated. 32 | 33 | ```rust,ignore 34 | {{#include ../../../examples/lazy-input/main.rs:main}} 35 | ``` 36 | -------------------------------------------------------------------------------- /benches/incremental.rs: -------------------------------------------------------------------------------- 1 | use std::hint::black_box; 2 | 3 | use codspeed_criterion_compat::{criterion_group, criterion_main, BatchSize, Criterion}; 4 | use salsa::Setter; 5 | 6 | #[salsa::input] 7 | struct Input { 8 | field: usize, 9 | } 10 | 11 | #[salsa::tracked] 12 | struct Tracked<'db> { 13 | number: usize, 14 | } 15 | 16 | #[salsa::tracked(returns(ref))] 17 | #[inline(never)] 18 | fn index<'db>(db: &'db dyn salsa::Database, input: Input) -> Vec> { 19 | (0..input.field(db)).map(|i| Tracked::new(db, i)).collect() 20 | } 21 | 22 | #[salsa::tracked] 23 | #[inline(never)] 24 | fn root(db: &dyn salsa::Database, input: Input) -> usize { 25 | let index = index(db, input); 26 | index.len() 27 | } 28 | 29 | fn many_tracked_structs(criterion: &mut Criterion) { 30 | criterion.bench_function("many_tracked_structs", |b| { 31 | b.iter_batched_ref( 32 | || { 33 | let db = salsa::DatabaseImpl::new(); 34 | 35 | let input = Input::new(black_box(&db), black_box(1_000)); 36 | let input2 = Input::new(black_box(&db), black_box(1)); 37 | 38 | // prewarm cache 39 | let root1 = root(black_box(&db), black_box(input)); 40 | assert_eq!(black_box(root1), 1_000); 41 | let root2 = root(black_box(&db), black_box(input2)); 42 | assert_eq!(black_box(root2), 1); 43 | 44 | (db, input, input2) 45 | }, 46 | |(db, input, input2)| { 47 | // Make a change, but fetch the result for the other input 48 | input2.set_field(black_box(db)).to(black_box(2)); 49 | 50 | let result = root(black_box(db), *black_box(input)); 51 | 52 | assert_eq!(black_box(result), 1_000); 53 | }, 54 | BatchSize::LargeInput, 55 | ); 56 | }); 57 | } 58 | 59 | criterion_group!(benches, many_tracked_structs); 60 | criterion_main!(benches); 61 | -------------------------------------------------------------------------------- /src/function/delete.rs: -------------------------------------------------------------------------------- 1 | use std::ptr::NonNull; 2 | 3 | use crate::function::memo::Memo; 4 | use crate::function::Configuration; 5 | 6 | /// Stores the list of memos that have been deleted so they can be freed 7 | /// once the next revision starts. See the comment on the field 8 | /// `deleted_entries` of [`FunctionIngredient`][] for more details. 9 | pub(super) struct DeletedEntries { 10 | memos: boxcar::Vec>>, 11 | } 12 | 13 | #[allow(clippy::undocumented_unsafe_blocks)] // TODO(#697) document safety 14 | unsafe impl Send for SharedBox {} 15 | #[allow(clippy::undocumented_unsafe_blocks)] // TODO(#697) document safety 16 | unsafe impl Sync for SharedBox {} 17 | 18 | impl Default for DeletedEntries { 19 | fn default() -> Self { 20 | Self { 21 | memos: Default::default(), 22 | } 23 | } 24 | } 25 | 26 | impl DeletedEntries { 27 | /// # Safety 28 | /// 29 | /// The memo must be valid and safe to free when the `DeletedEntries` list is cleared or dropped. 30 | pub(super) unsafe fn push(&self, memo: NonNull>) { 31 | // Safety: The memo must be valid and safe to free when the `DeletedEntries` list is cleared or dropped. 32 | let memo = 33 | unsafe { std::mem::transmute::>, NonNull>>(memo) }; 34 | 35 | self.memos.push(SharedBox(memo)); 36 | } 37 | 38 | /// Free all deleted memos, keeping the list available for reuse. 39 | pub(super) fn clear(&mut self) { 40 | self.memos.clear(); 41 | } 42 | } 43 | 44 | /// A wrapper around `NonNull` that frees the allocation when it is dropped. 45 | struct SharedBox(NonNull); 46 | 47 | impl Drop for SharedBox { 48 | fn drop(&mut self) { 49 | // SAFETY: Guaranteed by the caller of `DeletedEntries::push`. 50 | unsafe { drop(Box::from_raw(self.0.as_ptr())) }; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/elided-lifetime-in-tracked-fn.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! Test that a `tracked` fn on a `salsa::input` 4 | //! compiles and executes successfully. 5 | 6 | mod common; 7 | use common::LogDatabase; 8 | use expect_test::expect; 9 | use salsa::Setter; 10 | use test_log::test; 11 | 12 | #[salsa::input(debug)] 13 | struct MyInput { 14 | field: u32, 15 | } 16 | 17 | #[salsa::tracked] 18 | fn final_result(db: &dyn LogDatabase, input: MyInput) -> u32 { 19 | db.push_log(format!("final_result({input:?})")); 20 | intermediate_result(db, input).field(db) * 2 21 | } 22 | 23 | #[salsa::tracked] 24 | struct MyTracked<'db> { 25 | field: u32, 26 | } 27 | 28 | #[salsa::tracked] 29 | fn intermediate_result(db: &dyn LogDatabase, input: MyInput) -> MyTracked<'_> { 30 | db.push_log(format!("intermediate_result({input:?})")); 31 | MyTracked::new(db, input.field(db) / 2) 32 | } 33 | 34 | #[test] 35 | fn execute() { 36 | let mut db = common::LoggerDatabase::default(); 37 | 38 | let input = MyInput::new(&db, 22); 39 | assert_eq!(final_result(&db, input), 22); 40 | db.assert_logs(expect![[r#" 41 | [ 42 | "final_result(MyInput { [salsa id]: Id(0), field: 22 })", 43 | "intermediate_result(MyInput { [salsa id]: Id(0), field: 22 })", 44 | ]"#]]); 45 | 46 | // Intermediate result is the same, so final result does 47 | // not need to be recomputed: 48 | input.set_field(&mut db).to(23); 49 | assert_eq!(final_result(&db, input), 22); 50 | db.assert_logs(expect![[r#" 51 | [ 52 | "intermediate_result(MyInput { [salsa id]: Id(0), field: 23 })", 53 | ]"#]]); 54 | 55 | input.set_field(&mut db).to(24); 56 | assert_eq!(final_result(&db, input), 24); 57 | db.assert_logs(expect![[r#" 58 | [ 59 | "intermediate_result(MyInput { [salsa id]: Id(0), field: 24 })", 60 | "final_result(MyInput { [salsa id]: Id(0), field: 24 })", 61 | ]"#]]); 62 | } 63 | -------------------------------------------------------------------------------- /tests/compile-fail/tracked_fn_incompatibles.rs: -------------------------------------------------------------------------------- 1 | use salsa::Database as Db; 2 | 3 | #[salsa::input] 4 | struct MyInput { 5 | field: u32, 6 | } 7 | 8 | #[salsa::tracked(data = Data)] 9 | fn tracked_fn_with_data(db: &dyn Db, input: MyInput) -> u32 { 10 | input.field(db) * 2 11 | } 12 | 13 | #[salsa::tracked(db = Db)] 14 | fn tracked_fn_with_db(db: &dyn Db, input: MyInput) -> u32 { 15 | input.field(db) * 2 16 | } 17 | 18 | #[salsa::tracked(revisions = 12)] 19 | fn tracked_fn_with_revisions(db: &dyn Db, input: MyInput) -> u32 { 20 | input.field(db) * 2 21 | } 22 | 23 | #[salsa::tracked(constructor = TrackedFn3)] 24 | fn tracked_fn_with_constructor(db: &dyn Db, input: MyInput) -> u32 { 25 | input.field(db) * 2 26 | } 27 | 28 | #[salsa::tracked] 29 | fn tracked_fn_with_one_input(db: &dyn Db) -> u32 {} 30 | 31 | #[salsa::tracked] 32 | fn tracked_fn_with_receiver_not_applied_to_impl_block(&self, db: &dyn Db) -> u32 {} 33 | 34 | #[salsa::tracked(specify)] 35 | fn tracked_fn_with_too_many_arguments_for_specify( 36 | db: &dyn Db, 37 | input: MyInput, 38 | input: MyInput, 39 | ) -> u32 { 40 | } 41 | 42 | #[salsa::interned] 43 | struct MyInterned<'db> { 44 | field: u32, 45 | } 46 | 47 | #[salsa::tracked] 48 | fn tracked_fn_with_lt_param_and_elided_lt_on_db_arg1<'db>( 49 | db: &dyn Db, 50 | interned: MyInterned<'db>, 51 | ) -> u32 { 52 | interned.field(db) * 2 53 | } 54 | 55 | #[salsa::tracked] 56 | fn tracked_fn_with_lt_param_and_elided_lt_on_db_arg2<'db_lifetime>( 57 | db: &dyn Db, 58 | interned: MyInterned<'db_lifetime>, 59 | ) -> u32 { 60 | interned.field(db) * 2 61 | } 62 | 63 | #[salsa::tracked] 64 | fn tracked_fn_with_lt_param_and_elided_lt_on_input<'db>( 65 | db: &'db dyn Db, 66 | interned: MyInterned, 67 | ) -> u32 { 68 | interned.field(db) * 2 69 | } 70 | 71 | #[salsa::tracked] 72 | fn tracked_fn_with_multiple_lts<'db1, 'db2>(db: &'db1 dyn Db, interned: MyInterned<'db2>) -> u32 { 73 | interned.field(db) * 2 74 | } 75 | 76 | fn main() {} 77 | -------------------------------------------------------------------------------- /tests/debug_db_contents.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | #[salsa::interned(debug)] 4 | struct InternedStruct<'db> { 5 | name: String, 6 | } 7 | 8 | #[salsa::input(debug)] 9 | struct InputStruct { 10 | field: u32, 11 | } 12 | 13 | #[salsa::tracked(debug)] 14 | struct TrackedStruct<'db> { 15 | field: u32, 16 | } 17 | 18 | #[salsa::tracked] 19 | fn tracked_fn(db: &dyn salsa::Database, input: InputStruct) -> TrackedStruct<'_> { 20 | TrackedStruct::new(db, input.field(db) * 2) 21 | } 22 | 23 | #[test] 24 | fn execute() { 25 | use salsa::plumbing::ZalsaDatabase; 26 | let db = salsa::DatabaseImpl::new(); 27 | 28 | let interned1 = InternedStruct::new(&db, "Salsa".to_string()); 29 | let interned2 = InternedStruct::new(&db, "Salsa2".to_string()); 30 | 31 | // test interned structs 32 | let interned = InternedStruct::ingredient(db.zalsa()) 33 | .entries(db.zalsa()) 34 | .collect::>(); 35 | 36 | assert_eq!(interned.len(), 2); 37 | assert_eq!(interned[0].as_struct(), interned1); 38 | assert_eq!(interned[1].as_struct(), interned2); 39 | assert_eq!(interned[0].value().fields().0, "Salsa"); 40 | assert_eq!(interned[1].value().fields().0, "Salsa2"); 41 | 42 | // test input structs 43 | let input1 = InputStruct::new(&db, 22); 44 | 45 | let inputs = InputStruct::ingredient(&db) 46 | .entries(db.zalsa()) 47 | .collect::>(); 48 | 49 | assert_eq!(inputs.len(), 1); 50 | assert_eq!(inputs[0].as_struct(), input1); 51 | assert_eq!(inputs[0].value().fields().0, 22); 52 | 53 | // test tracked structs 54 | let tracked1 = tracked_fn(&db, input1); 55 | assert_eq!(tracked1.field(&db), 44); 56 | 57 | let tracked = TrackedStruct::ingredient(&db) 58 | .entries(db.zalsa()) 59 | .collect::>(); 60 | 61 | assert_eq!(tracked.len(), 1); 62 | assert_eq!(tracked[0].as_struct(), tracked1); 63 | assert_eq!(tracked[0].value().fields().0, tracked1.field(&db)); 64 | } 65 | -------------------------------------------------------------------------------- /examples/calc/db.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | use std::sync::{Arc, Mutex}; 3 | 4 | // ANCHOR: db_struct 5 | #[salsa::db] 6 | #[derive(Clone)] 7 | #[cfg_attr(not(test), derive(Default))] 8 | pub struct CalcDatabaseImpl { 9 | storage: salsa::Storage, 10 | 11 | // The logs are only used for testing and demonstrating reuse: 12 | #[cfg(test)] 13 | logs: Arc>>>, 14 | } 15 | 16 | #[cfg(test)] 17 | impl Default for CalcDatabaseImpl { 18 | fn default() -> Self { 19 | let logs = >>>>::default(); 20 | Self { 21 | storage: salsa::Storage::new(Some(Box::new({ 22 | let logs = logs.clone(); 23 | move |event| { 24 | eprintln!("Event: {event:?}"); 25 | // Log interesting events, if logging is enabled 26 | if let Some(logs) = &mut *logs.lock().unwrap() { 27 | // only log interesting events 28 | if let salsa::EventKind::WillExecute { .. } = event.kind { 29 | logs.push(format!("Event: {event:?}")); 30 | } 31 | } 32 | } 33 | }))), 34 | logs, 35 | } 36 | } 37 | } 38 | // ANCHOR_END: db_struct 39 | 40 | impl CalcDatabaseImpl { 41 | /// Enable logging of each salsa event. 42 | #[cfg(test)] 43 | pub fn enable_logging(&self) { 44 | let mut logs = self.logs.lock().unwrap(); 45 | if logs.is_none() { 46 | *logs = Some(vec![]); 47 | } 48 | } 49 | 50 | #[cfg(test)] 51 | #[allow(unused)] 52 | pub fn take_logs(&self) -> Vec { 53 | let mut logs = self.logs.lock().unwrap(); 54 | if let Some(logs) = &mut *logs { 55 | std::mem::take(logs) 56 | } else { 57 | vec![] 58 | } 59 | } 60 | } 61 | 62 | // ANCHOR: db_impl 63 | #[salsa::db] 64 | impl salsa::Database for CalcDatabaseImpl {} 65 | // ANCHOR_END: db_impl 66 | -------------------------------------------------------------------------------- /book/src/tutorial/debug.md: -------------------------------------------------------------------------------- 1 | # Defining the parser: debug impls and testing 2 | 3 | As the final part of the parser, we need to write some tests. 4 | To do so, we will create a database, set the input source text, run the parser, and check the result. 5 | Before we can do that, though, we have to address one question: how do we inspect the value of an interned type like `Expression`? 6 | 7 | ## The `DebugWithDb` trait 8 | 9 | Because an interned type like `Expression` just stores an integer, the traditional `Debug` trait is not very useful. 10 | To properly print a `Expression`, you need to access the Salsa database to find out what its value is. 11 | To solve this, `salsa` provides a `DebugWithDb` trait that acts like the regular `Debug`, but takes a database as argument. 12 | For types that implement this trait, you can invoke the `debug` method. 13 | This returns a temporary that implements the ordinary `Debug` trait, allowing you to write something like 14 | 15 | ```rust 16 | eprintln!("Expression = {:?}", expr.debug(db)); 17 | ``` 18 | 19 | and get back the output you expect. 20 | 21 | The `DebugWithDb` trait is automatically derived for all `#[input]`, `#[interned]`, and `#[tracked]` structs. 22 | 23 | ## Forwarding to the ordinary `Debug` trait 24 | 25 | For consistency, it is sometimes useful to have a `DebugWithDb` implementation even for types, like `Op`, that are just ordinary enums. You can do that like so: 26 | 27 | ```rust 28 | {{#include ../../../examples/calc/ir.rs:op_debug_impl}} 29 | ``` 30 | 31 | ## Writing the unit test 32 | 33 | Now that we have our `DebugWithDb` impls in place, we can write a simple unit test harness. 34 | The `parse_string` function below creates a database, sets the source text, and then invokes the parser: 35 | 36 | ```rust 37 | {{#include ../../../examples/calc/parser.rs:parse_string}} 38 | ``` 39 | 40 | Combined with the [`expect-test`](https://crates.io/crates/expect-test) crate, we can then write unit tests like this one: 41 | 42 | ```rust 43 | {{#include ../../../examples/calc/parser.rs:parse_print}} 44 | ``` 45 | -------------------------------------------------------------------------------- /tests/compile-fail/tracked_fn_return_not_update.stderr: -------------------------------------------------------------------------------- 1 | error[E0369]: binary operation `==` cannot be applied to type `&NotUpdate` 2 | --> tests/compile-fail/tracked_fn_return_not_update.rs:10:56 3 | | 4 | 10 | fn tracked_fn<'db>(db: &'db dyn Db, input: MyInput) -> NotUpdate { 5 | | ^^^^^^^^^ 6 | | 7 | note: an implementation of `PartialEq` might be missing for `NotUpdate` 8 | --> tests/compile-fail/tracked_fn_return_not_update.rs:7:1 9 | | 10 | 7 | struct NotUpdate; 11 | | ^^^^^^^^^^^^^^^^ must implement `PartialEq` 12 | help: consider annotating `NotUpdate` with `#[derive(PartialEq)]` 13 | | 14 | 7 + #[derive(PartialEq)] 15 | 8 | struct NotUpdate; 16 | | 17 | 18 | error[E0599]: the function or associated item `maybe_update` exists for struct `UpdateDispatch`, but its trait bounds were not satisfied 19 | --> tests/compile-fail/tracked_fn_return_not_update.rs:10:56 20 | | 21 | 7 | struct NotUpdate; 22 | | ---------------- doesn't satisfy `NotUpdate: PartialEq` or `NotUpdate: Update` 23 | ... 24 | 10 | fn tracked_fn<'db>(db: &'db dyn Db, input: MyInput) -> NotUpdate { 25 | | ^^^^^^^^^ function or associated item cannot be called on `UpdateDispatch` due to unsatisfied trait bounds 26 | | 27 | ::: src/update.rs 28 | | 29 | | pub struct Dispatch(PhantomData); 30 | | ---------------------- doesn't satisfy `_: UpdateFallback` 31 | | 32 | = note: the following trait bounds were not satisfied: 33 | `NotUpdate: Update` 34 | `NotUpdate: PartialEq` 35 | which is required by `UpdateDispatch: UpdateFallback` 36 | note: the trait `Update` must be implemented 37 | --> src/update.rs 38 | | 39 | | pub unsafe trait Update { 40 | | ^^^^^^^^^^^^^^^^^^^^^^^ 41 | help: consider annotating `NotUpdate` with `#[derive(PartialEq)]` 42 | | 43 | 7 + #[derive(PartialEq)] 44 | 8 | struct NotUpdate; 45 | | 46 | -------------------------------------------------------------------------------- /src/return_mode.rs: -------------------------------------------------------------------------------- 1 | //! User-implementable salsa traits for refining the return type via `returns(as_ref)` and `returns(as_deref)`. 2 | 3 | use std::ops::Deref; 4 | 5 | /// Used to determine the return type and value for tracked fields and functions annotated with `returns(as_ref)`. 6 | pub trait SalsaAsRef { 7 | // The type returned by tracked fields and functions annotated with `returns(as_ref)`. 8 | type AsRef<'a> 9 | where 10 | Self: 'a; 11 | 12 | // The value returned by tracked fields and functions annotated with `returns(as_ref)`. 13 | fn as_ref(&self) -> Self::AsRef<'_>; 14 | } 15 | 16 | impl SalsaAsRef for Option { 17 | type AsRef<'a> 18 | = Option<&'a T> 19 | where 20 | Self: 'a; 21 | 22 | fn as_ref(&self) -> Self::AsRef<'_> { 23 | self.as_ref() 24 | } 25 | } 26 | 27 | impl SalsaAsRef for Result { 28 | type AsRef<'a> 29 | = Result<&'a T, &'a E> 30 | where 31 | Self: 'a; 32 | 33 | fn as_ref(&self) -> Self::AsRef<'_> { 34 | self.as_ref() 35 | } 36 | } 37 | 38 | /// Used to determine the return type and value for tracked fields and functions annotated with `returns(as_deref)`. 39 | pub trait SalsaAsDeref { 40 | // The type returned by tracked fields and functions annotated with `returns(as_deref)`. 41 | type AsDeref<'a> 42 | where 43 | Self: 'a; 44 | 45 | // The value returned by tracked fields and functions annotated with `returns(as_deref)`. 46 | fn as_deref(&self) -> Self::AsDeref<'_>; 47 | } 48 | 49 | impl SalsaAsDeref for Option { 50 | type AsDeref<'a> 51 | = Option<&'a T::Target> 52 | where 53 | Self: 'a; 54 | 55 | fn as_deref(&self) -> Self::AsDeref<'_> { 56 | self.as_deref() 57 | } 58 | } 59 | 60 | impl SalsaAsDeref for Result { 61 | type AsDeref<'a> 62 | = Result<&'a T::Target, &'a E> 63 | where 64 | Self: 'a; 65 | 66 | fn as_deref(&self) -> Self::AsDeref<'_> { 67 | self.as_deref() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/input/setter.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use crate::input::{Configuration, IngredientImpl}; 4 | use crate::{Durability, Runtime}; 5 | 6 | /// Setter for a field of an input. 7 | pub trait Setter: Sized { 8 | type FieldTy; 9 | fn with_durability(self, durability: Durability) -> Self; 10 | fn to(self, value: Self::FieldTy) -> Self::FieldTy; 11 | } 12 | 13 | #[must_use] 14 | pub struct SetterImpl<'setter, C: Configuration, S, F> { 15 | runtime: &'setter mut Runtime, 16 | id: C::Struct, 17 | ingredient: &'setter mut IngredientImpl, 18 | durability: Option, 19 | field_index: usize, 20 | setter: S, 21 | phantom: PhantomData, 22 | } 23 | 24 | impl<'setter, C, S, F> SetterImpl<'setter, C, S, F> 25 | where 26 | C: Configuration, 27 | S: FnOnce(&mut C::Fields, F) -> F, 28 | { 29 | pub fn new( 30 | runtime: &'setter mut Runtime, 31 | id: C::Struct, 32 | field_index: usize, 33 | ingredient: &'setter mut IngredientImpl, 34 | setter: S, 35 | ) -> Self { 36 | SetterImpl { 37 | runtime, 38 | id, 39 | field_index, 40 | ingredient, 41 | durability: None, 42 | setter, 43 | phantom: PhantomData, 44 | } 45 | } 46 | } 47 | 48 | impl Setter for SetterImpl<'_, C, S, F> 49 | where 50 | C: Configuration, 51 | S: FnOnce(&mut C::Fields, F) -> F, 52 | { 53 | type FieldTy = F; 54 | 55 | fn with_durability(mut self, durability: Durability) -> Self { 56 | self.durability = Some(durability); 57 | self 58 | } 59 | 60 | fn to(self, value: F) -> F { 61 | let Self { 62 | runtime, 63 | id, 64 | ingredient, 65 | durability, 66 | field_index, 67 | setter, 68 | phantom: _, 69 | } = self; 70 | 71 | ingredient.set_field(runtime, id, field_index, durability, |tuple| { 72 | setter(tuple, value) 73 | }) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/compile-fail/accumulator_incompatibles.stderr: -------------------------------------------------------------------------------- 1 | error: `returns` option not allowed here 2 | --> tests/compile-fail/accumulator_incompatibles.rs:1:22 3 | | 4 | 1 | #[salsa::accumulator(returns(ref))] 5 | | ^^^^^^^ 6 | 7 | error: `specify` option not allowed here 8 | --> tests/compile-fail/accumulator_incompatibles.rs:4:22 9 | | 10 | 4 | #[salsa::accumulator(specify)] 11 | | ^^^^^^^ 12 | 13 | error: `no_eq` option not allowed here 14 | --> tests/compile-fail/accumulator_incompatibles.rs:7:22 15 | | 16 | 7 | #[salsa::accumulator(no_eq)] 17 | | ^^^^^ 18 | 19 | error: `data` option not allowed here 20 | --> tests/compile-fail/accumulator_incompatibles.rs:10:22 21 | | 22 | 10 | #[salsa::accumulator(data = MyAcc)] 23 | | ^^^^ 24 | 25 | error: `db` option not allowed here 26 | --> tests/compile-fail/accumulator_incompatibles.rs:13:22 27 | | 28 | 13 | #[salsa::accumulator(db = Db)] 29 | | ^^ 30 | 31 | error: unrecognized option `recover_fn` 32 | --> tests/compile-fail/accumulator_incompatibles.rs:16:22 33 | | 34 | 16 | #[salsa::accumulator(recover_fn = recover)] 35 | | ^^^^^^^^^^ 36 | 37 | error: `lru` option not allowed here 38 | --> tests/compile-fail/accumulator_incompatibles.rs:19:22 39 | | 40 | 19 | #[salsa::accumulator(lru = 12)] 41 | | ^^^ 42 | 43 | error: `revisions` option not allowed here 44 | --> tests/compile-fail/accumulator_incompatibles.rs:22:22 45 | | 46 | 22 | #[salsa::accumulator(revisions = 12)] 47 | | ^^^^^^^^^ 48 | 49 | error: `constructor` option not allowed here 50 | --> tests/compile-fail/accumulator_incompatibles.rs:25:22 51 | | 52 | 25 | #[salsa::accumulator(constructor = Constructor)] 53 | | ^^^^^^^^^^^ 54 | 55 | error: `heap_size` option not allowed here 56 | --> tests/compile-fail/accumulator_incompatibles.rs:28:22 57 | | 58 | 28 | #[salsa::accumulator(heap_size = size)] 59 | | ^^^^^^^^^ 60 | -------------------------------------------------------------------------------- /tests/expect_reuse_field_x_of_an_input_changes_but_fn_depends_on_field_y.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | //! Test that if field X of an input changes but not field Y, 4 | //! functions that depend on X re-execute, but those depending only on Y do not 5 | //! compiles and executes successfully. 6 | #![allow(dead_code)] 7 | 8 | mod common; 9 | use common::LogDatabase; 10 | use expect_test::expect; 11 | use salsa::Setter; 12 | 13 | #[salsa::input(debug)] 14 | struct MyInput { 15 | x: u32, 16 | y: u32, 17 | } 18 | 19 | #[salsa::tracked] 20 | fn result_depends_on_x(db: &dyn LogDatabase, input: MyInput) -> u32 { 21 | db.push_log(format!("result_depends_on_x({input:?})")); 22 | input.x(db) + 1 23 | } 24 | 25 | #[salsa::tracked] 26 | fn result_depends_on_y(db: &dyn LogDatabase, input: MyInput) -> u32 { 27 | db.push_log(format!("result_depends_on_y({input:?})")); 28 | input.y(db) - 1 29 | } 30 | 31 | #[test] 32 | fn execute() { 33 | // result_depends_on_x = x + 1 34 | // result_depends_on_y = y - 1 35 | let mut db = common::LoggerDatabase::default(); 36 | 37 | let input = MyInput::new(&db, 22, 33); 38 | assert_eq!(result_depends_on_x(&db, input), 23); 39 | db.assert_logs(expect![[r#" 40 | [ 41 | "result_depends_on_x(MyInput { [salsa id]: Id(0), x: 22, y: 33 })", 42 | ]"#]]); 43 | 44 | assert_eq!(result_depends_on_y(&db, input), 32); 45 | db.assert_logs(expect![[r#" 46 | [ 47 | "result_depends_on_y(MyInput { [salsa id]: Id(0), x: 22, y: 33 })", 48 | ]"#]]); 49 | 50 | input.set_x(&mut db).to(23); 51 | // input x changes, so result depends on x needs to be recomputed; 52 | assert_eq!(result_depends_on_x(&db, input), 24); 53 | db.assert_logs(expect![[r#" 54 | [ 55 | "result_depends_on_x(MyInput { [salsa id]: Id(0), x: 23, y: 33 })", 56 | ]"#]]); 57 | 58 | // input y is the same, so result depends on y 59 | // does not need to be recomputed; 60 | assert_eq!(result_depends_on_y(&db, input), 32); 61 | db.assert_logs(expect!["[]"]); 62 | } 63 | -------------------------------------------------------------------------------- /tests/tracked_fn_on_input_with_high_durability.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | #![allow(warnings)] 3 | 4 | use common::{EventLoggerDatabase, HasLogger, LogDatabase, Logger}; 5 | use expect_test::expect; 6 | use salsa::plumbing::HasStorage; 7 | use salsa::{Database, Durability, Event, EventKind, Setter}; 8 | 9 | mod common; 10 | #[salsa::input] 11 | struct MyInput { 12 | field: u32, 13 | } 14 | 15 | #[salsa::tracked] 16 | fn tracked_fn(db: &dyn salsa::Database, input: MyInput) -> u32 { 17 | input.field(db) * 2 18 | } 19 | 20 | #[test] 21 | fn execute() { 22 | let mut db = EventLoggerDatabase::default(); 23 | let input_low = MyInput::new(&db, 22); 24 | let input_high = MyInput::builder(2200).durability(Durability::HIGH).new(&db); 25 | 26 | assert_eq!(tracked_fn(&db, input_low), 44); 27 | assert_eq!(tracked_fn(&db, input_high), 4400); 28 | 29 | db.assert_logs(expect![[r#" 30 | [ 31 | "WillCheckCancellation", 32 | "WillExecute { database_key: tracked_fn(Id(0)) }", 33 | "WillCheckCancellation", 34 | "WillExecute { database_key: tracked_fn(Id(1)) }", 35 | ]"#]]); 36 | 37 | db.synthetic_write(Durability::LOW); 38 | 39 | assert_eq!(tracked_fn(&db, input_low), 44); 40 | assert_eq!(tracked_fn(&db, input_high), 4400); 41 | 42 | // FIXME: There's currently no good way to verify whether an input was validated using shallow or deep comparison. 43 | // All we can do for now is verify that the values were validated. 44 | // Note: It maybe confusing why it validates `input_high` when the write has `Durability::LOW`. 45 | // This is because all values must be validated whenever a write occurs. It doesn't mean that it 46 | // executed the query. 47 | db.assert_logs(expect![[r#" 48 | [ 49 | "DidSetCancellationFlag", 50 | "WillCheckCancellation", 51 | "DidValidateMemoizedValue { database_key: tracked_fn(Id(0)) }", 52 | "WillCheckCancellation", 53 | "DidValidateMemoizedValue { database_key: tracked_fn(Id(1)) }", 54 | ]"#]]); 55 | } 56 | -------------------------------------------------------------------------------- /tests/tracked_struct_manual_update.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "inventory")] 2 | 3 | mod common; 4 | 5 | use std::sync::atomic::{AtomicBool, Ordering}; 6 | 7 | use salsa::{Database, Setter}; 8 | 9 | static MARK1: AtomicBool = AtomicBool::new(false); 10 | static MARK2: AtomicBool = AtomicBool::new(false); 11 | 12 | #[salsa::tracked] 13 | struct Tracked<'db> { 14 | #[tracked] 15 | #[maybe_update(|dst, src| { 16 | *dst = src; 17 | MARK1.store(true, Ordering::Release); 18 | true 19 | })] 20 | tracked: usize, 21 | #[maybe_update(untracked_update)] 22 | untracked: usize, 23 | } 24 | 25 | unsafe fn untracked_update(dst: *mut usize, src: usize) -> bool { 26 | unsafe { *dst = src }; 27 | MARK2.store(true, Ordering::Release); 28 | true 29 | } 30 | 31 | #[salsa::input] 32 | struct MyInput { 33 | field1: usize, 34 | field2: usize, 35 | } 36 | 37 | #[salsa::tracked] 38 | fn intermediate(db: &dyn salsa::Database, input: MyInput) -> Tracked<'_> { 39 | Tracked::new(db, input.field1(db), input.field2(db)) 40 | } 41 | 42 | #[salsa::tracked] 43 | fn accumulate(db: &dyn salsa::Database, input: MyInput) -> (usize, usize) { 44 | let tracked = intermediate(db, input); 45 | let one = read_tracked(db, tracked); 46 | let two = read_untracked(db, tracked); 47 | 48 | (one, two) 49 | } 50 | 51 | #[salsa::tracked] 52 | fn read_tracked<'db>(db: &'db dyn Database, tracked: Tracked<'db>) -> usize { 53 | tracked.tracked(db) 54 | } 55 | 56 | #[salsa::tracked] 57 | fn read_untracked<'db>(db: &'db dyn Database, tracked: Tracked<'db>) -> usize { 58 | tracked.untracked(db) 59 | } 60 | 61 | #[test] 62 | fn execute() { 63 | let mut db = salsa::DatabaseImpl::default(); 64 | let input = MyInput::new(&db, 1, 1); 65 | 66 | assert_eq!(accumulate(&db, input), (1, 1)); 67 | 68 | assert!(!MARK1.load(Ordering::Acquire)); 69 | assert!(!MARK2.load(Ordering::Acquire)); 70 | 71 | input.set_field1(&mut db).to(2); 72 | assert_eq!(accumulate(&db, input), (2, 1)); 73 | 74 | assert!(MARK1.load(Ordering::Acquire)); 75 | assert!(MARK2.load(Ordering::Acquire)); 76 | } 77 | -------------------------------------------------------------------------------- /.github/workflows/book.yml: -------------------------------------------------------------------------------- 1 | name: Book 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | paths: 9 | - "book/**" 10 | - ".github/workflows/book.yml" 11 | 12 | jobs: 13 | book: 14 | name: Book 15 | runs-on: ubuntu-latest 16 | env: 17 | MDBOOK_VERSION: "0.4.40" 18 | MDBOOK_LINKCHECK_VERSION: "0.7.7" 19 | MDBOOK_MERMAID_VERSION: "0.13.0" 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Install mdbook 23 | run: | 24 | curl -L https://github.com/rust-lang/mdBook/releases/download/v$MDBOOK_VERSION/mdbook-v$MDBOOK_VERSION-x86_64-unknown-linux-gnu.tar.gz | tar xz -C ~/.cargo/bin 25 | curl -L https://github.com/badboy/mdbook-mermaid/releases/download/v$MDBOOK_MERMAID_VERSION/mdbook-mermaid-v$MDBOOK_MERMAID_VERSION-x86_64-unknown-linux-gnu.tar.gz | tar xz -C ~/.cargo/bin 26 | curl -L https://github.com/Michael-F-Bryan/mdbook-linkcheck/releases/download/v$MDBOOK_LINKCHECK_VERSION/mdbook-linkcheck.x86_64-unknown-linux-gnu.zip -O 27 | unzip mdbook-linkcheck.x86_64-unknown-linux-gnu.zip -d ~/.cargo/bin 28 | chmod +x ~/.cargo/bin/mdbook-linkcheck 29 | - name: Setup Pages 30 | id: pages 31 | uses: actions/configure-pages@v5 32 | - name: Build 33 | run: mdbook build 34 | working-directory: book 35 | - name: Upload static files as artifact 36 | id: deployment 37 | uses: actions/upload-pages-artifact@v3 38 | with: 39 | path: ./book/book/html 40 | deploy: 41 | name: Deploy 42 | runs-on: ubuntu-latest 43 | needs: book 44 | if: github.event_name == 'push' && github.ref == 'refs/heads/master' 45 | concurrency: 46 | group: github-pages 47 | cancel-in-progress: true 48 | permissions: 49 | contents: read 50 | pages: write 51 | id-token: write 52 | environment: 53 | name: github-pages 54 | url: ${{ steps.deployment.outputs.page_url }} 55 | steps: 56 | - name: Deploy to GitHub Pages 57 | id: deployment 58 | uses: actions/deploy-pages@v4 59 | -------------------------------------------------------------------------------- /tests/accumulate-from-tracked-fn.rs: -------------------------------------------------------------------------------- 1 | #![cfg(all(feature = "inventory", feature = "accumulator"))] 2 | 3 | //! Accumulate values from within a tracked function. 4 | //! Then mutate the values so that the tracked function re-executes. 5 | //! Check that we accumulate the appropriate, new values. 6 | 7 | use expect_test::expect; 8 | use salsa::{Accumulator, Setter}; 9 | use test_log::test; 10 | 11 | #[salsa::input(debug)] 12 | struct List { 13 | value: u32, 14 | next: Option, 15 | } 16 | 17 | #[salsa::accumulator] 18 | #[derive(Copy, Clone, Debug)] 19 | struct Integers(u32); 20 | 21 | #[salsa::tracked] 22 | fn compute(db: &dyn salsa::Database, input: List) { 23 | eprintln!( 24 | "{:?}(value={:?}, next={:?})", 25 | input, 26 | input.value(db), 27 | input.next(db) 28 | ); 29 | let result = if let Some(next) = input.next(db) { 30 | let next_integers = compute::accumulated::(db, next); 31 | eprintln!("{next_integers:?}"); 32 | let v = input.value(db) + next_integers.iter().map(|a| a.0).sum::(); 33 | eprintln!("input={:?} v={:?}", input.value(db), v); 34 | v 35 | } else { 36 | input.value(db) 37 | }; 38 | Integers(result).accumulate(db); 39 | eprintln!("pushed result {result:?}"); 40 | } 41 | 42 | #[test] 43 | fn test1() { 44 | let mut db = salsa::DatabaseImpl::new(); 45 | 46 | let l0 = List::new(&db, 1, None); 47 | let l1 = List::new(&db, 10, Some(l0)); 48 | 49 | compute(&db, l1); 50 | expect![[r#" 51 | [ 52 | Integers( 53 | 11, 54 | ), 55 | Integers( 56 | 1, 57 | ), 58 | ] 59 | "#]] 60 | .assert_debug_eq(&compute::accumulated::(&db, l1)); 61 | 62 | l0.set_value(&mut db).to(2); 63 | compute(&db, l1); 64 | expect![[r#" 65 | [ 66 | Integers( 67 | 12, 68 | ), 69 | Integers( 70 | 2, 71 | ), 72 | ] 73 | "#]] 74 | .assert_debug_eq(&compute::accumulated::(&db, l1)); 75 | } 76 | --------------------------------------------------------------------------------