Repository: salsa-rs/salsa (rust)

Issue:
Add "builder style" setter APIs that permit setting durability
The `salsa::input` structs generate a setter for each field. We should make an alternate version of the setter that takes a durability. The obvious thing is something like `set_foo_with_durability`, but now that I think about it, maybe a builder API would be more appropriate, in order to be more future proof.

I'm inclined to modify the setters to be builder style. You would write `input.set_foo(&mut db)` and that would return a `salsa::Setter` struct that captures the input, a reference to the field (a `FunctionIngredient`), and the database. The setter would have a `to_value(self, value)` that stores the new value, so that the "normal" call looks like

```rust
input.set_foo(&mut db).to_value(value)
```

but then we can add additional builder style methods, e.g.

```rust
input.set_foo(&mut db).with_durability(Durability::HIGH).to_value(value)
```

This makes those method highly discoverable and easily documented. It also means we can add new "final actions", e.g. maybe for "taking", deleting, or something else in the future.

Pull request description:
Implement Setter API
Fixes #329 

Patch:
diff --git a/calc-example/calc/src/type_check.rs b/calc-example/calc/src/type_check.rs
index f62b2086..a438a9d0 100644
--- a/calc-example/calc/src/type_check.rs
+++ b/calc-example/calc/src/type_check.rs
@@ -114,7 +114,9 @@ fn check_string(
 
     // Apply edits and check diagnostics/logs after each one
     for (new_source_text, expected_diagnostics, expected_logs) in edits {
-        source_program.set_text(&mut db, new_source_text.to_string());
+        source_program
+            .set_text(&mut db)
+            .to(new_source_text.to_string());
         let program = parse_statements(&db, source_program);
         expected_diagnostics.assert_debug_eq(&type_check_program::accumulated::<Diagnostics>(
             &db, program,
diff --git a/components/salsa-2022-macros/src/input.rs b/components/salsa-2022-macros/src/input.rs
index 9932226b..2f85db8d 100644
--- a/components/salsa-2022-macros/src/input.rs
+++ b/components/salsa-2022-macros/src/input.rs
@@ -94,11 +94,11 @@ impl InputStruct {
         let set_field_names = self.all_set_field_names();
         let field_setters: Vec<syn::ImplItemMethod> = field_indices.iter().zip(&set_field_names).zip(&field_tys).map(|((field_index, set_field_name), field_ty)| {
             parse_quote! {
-                pub fn #set_field_name<'db>(self, __db: &'db mut #db_dyn_ty, __value: #field_ty) -> #field_ty
+                pub fn #set_field_name<'db>(self, __db: &'db mut #db_dyn_ty) -> salsa::setter::Setter<'db, #ident, #field_ty>
                 {
                     let (__jar, __runtime) = <_ as salsa::storage::HasJar<#jar_ty>>::jar_mut(__db);
                     let __ingredients = <#jar_ty as salsa::storage::HasIngredientsFor< #ident >>::ingredient_mut(__jar);
-                    __ingredients.#field_index.store(__runtime, self, __value, salsa::Durability::LOW).unwrap()
+                    salsa::setter::Setter::new(__runtime, self, &mut __ingredients.#field_index)
                 }
             }
         })
diff --git a/components/salsa-2022-macros/src/tracked_fn.rs b/components/salsa-2022-macros/src/tracked_fn.rs
index f8d262f7..aa724af7 100644
--- a/components/salsa-2022-macros/src/tracked_fn.rs
+++ b/components/salsa-2022-macros/src/tracked_fn.rs
@@ -426,7 +426,7 @@ fn setter_fn(
 ///
 /// # Examples
 ///
-/// ```rust
+/// ```rust,ignore
 /// #[salsa::tracked(lru=32)]
 /// fn my_tracked_fn(db: &dyn crate::Db, ...) { }
 ///
diff --git a/components/salsa-2022/src/lib.rs b/components/salsa-2022/src/lib.rs
index 6a0e4ad8..d3fe4ddd 100644
--- a/components/salsa-2022/src/lib.rs
+++ b/components/salsa-2022/src/lib.rs
@@ -20,6 +20,7 @@ pub mod revision;
 pub mod routes;
 pub mod runtime;
 pub mod salsa_struct;
+pub mod setter;
 pub mod storage;
 #[doc(hidden)]
 pub mod tracked_struct;
diff --git a/components/salsa-2022/src/setter.rs b/components/salsa-2022/src/setter.rs
new file mode 100644
index 00000000..05af9c0e
--- /dev/null
+++ b/components/salsa-2022/src/setter.rs
@@ -0,0 +1,39 @@
+use crate::input_field::InputFieldIngredient;
+use crate::{AsId, Durability, Runtime};
+use std::hash::Hash;
+
+#[must_use]
+pub struct Setter<'setter, K, F> {
+    runtime: &'setter mut Runtime,
+    key: K,
+    ingredient: &'setter mut InputFieldIngredient<K, F>,
+    durability: Durability,
+}
+
+impl<'setter, K, F> Setter<'setter, K, F>
+where
+    K: Eq + Hash + AsId,
+{
+    pub fn new(
+        runtime: &'setter mut Runtime,
+        key: K,
+        ingredient: &'setter mut InputFieldIngredient<K, F>,
+    ) -> Self {
+        Setter {
+            runtime,
+            key,
+            ingredient,
+            durability: Durability::LOW,
+        }
+    }
+
+    pub fn with_durability(self, durability: Durability) -> Self {
+        Setter { durability, ..self }
+    }
+
+    pub fn to(self, value: F) -> F {
+        self.ingredient
+            .store(self.runtime, self.key, value, self.durability)
+            .unwrap()
+    }
+}


Test patch:
diff --git a/salsa-2022-tests/tests/accumulate-from-tracked-fn.rs b/salsa-2022-tests/tests/accumulate-from-tracked-fn.rs
index 85559e44..92c832b5 100644
--- a/salsa-2022-tests/tests/accumulate-from-tracked-fn.rs
+++ b/salsa-2022-tests/tests/accumulate-from-tracked-fn.rs
@@ -81,7 +81,7 @@ fn test1() {
     "#]]
     .assert_debug_eq(&compute::accumulated::<Integers>(&db, l1));
 
-    l0.set_value(&mut db, 2);
+    l0.set_value(&mut db).to(2);
     compute(&db, l1);
     expect![[r#"
         [
diff --git a/salsa-2022-tests/tests/accumulate-reuse-workaround.rs b/salsa-2022-tests/tests/accumulate-reuse-workaround.rs
index fa3f5af6..6a1421dc 100644
--- a/salsa-2022-tests/tests/accumulate-reuse-workaround.rs
+++ b/salsa-2022-tests/tests/accumulate-reuse-workaround.rs
@@ -87,7 +87,7 @@ fn test1() {
     // When we mutate `l1`, we should re-execute `compute` for `l1`,
     // and we re-execute accumulated for `l1`, but we do NOT re-execute
     // `compute` for `l2`.
-    l1.set_value(&mut db, 2);
+    l1.set_value(&mut db).to(2);
     assert_eq!(compute(&db, l2), 2);
     db.assert_logs(expect![[r#"
         [
diff --git a/salsa-2022-tests/tests/accumulate-reuse.rs b/salsa-2022-tests/tests/accumulate-reuse.rs
index f8258c40..a2fc5cda 100644
--- a/salsa-2022-tests/tests/accumulate-reuse.rs
+++ b/salsa-2022-tests/tests/accumulate-reuse.rs
@@ -82,7 +82,7 @@ fn test1() {
     // but we should not have to re-execute `compute` for `l2`.
     // The only inpout for `compute(l1)` is the accumulated values from `l1`,
     // which have not changed.
-    l1.set_value(&mut db, 2);
+    l1.set_value(&mut db).to(2);
     assert_eq!(compute(&db, l2), 2);
     db.assert_logs(expect![[r#"
         [
diff --git a/salsa-2022-tests/tests/accumulate.rs b/salsa-2022-tests/tests/accumulate.rs
index 86932f8c..257bd802 100644
--- a/salsa-2022-tests/tests/accumulate.rs
+++ b/salsa-2022-tests/tests/accumulate.rs
@@ -132,7 +132,7 @@ fn change_a_and_reaccumulate() {
         ]"#]]);
 
     // Change to `a = 1`, which means `push_logs` does not call `push_a_logs` at all
-    input.set_field_a(&mut db, 1);
+    input.set_field_a(&mut db).to(1);
     let logs = push_logs::accumulated::<Logs>(&db, input);
     expect![[r#"
         [
@@ -167,7 +167,7 @@ fn get_a_logs_after_changing_b() {
 
     // Changing `b` does not cause `push_a_logs` to re-execute
     // and we still get the same result
-    input.set_field_b(&mut db, 5);
+    input.set_field_b(&mut db).to(5);
     let logs = push_a_logs::accumulated::<Logs>(&db, input);
     expect![[r#"
         [
diff --git a/salsa-2022-tests/tests/cycles.rs b/salsa-2022-tests/tests/cycles.rs
index 5a88ed9a..b07b65fb 100644
--- a/salsa-2022-tests/tests/cycles.rs
+++ b/salsa-2022-tests/tests/cycles.rs
@@ -248,7 +248,7 @@ fn cycle_revalidate() {
     let mut db = Database::default();
     let abc = ABC::new(&mut db, CycleQuery::B, CycleQuery::A, CycleQuery::None);
     assert!(cycle_a(&db, abc).is_err());
-    abc.set_b(&mut db, CycleQuery::A); // same value as default
+    abc.set_b(&mut db).to(CycleQuery::A); // same value as default
     assert!(cycle_a(&db, abc).is_err());
 }
 
@@ -261,7 +261,7 @@ fn cycle_recovery_unchanged_twice() {
     let abc = ABC::new(&mut db, CycleQuery::B, CycleQuery::A, CycleQuery::None);
     assert!(cycle_a(&db, abc).is_err());
 
-    abc.set_c(&mut db, CycleQuery::A); // force new revision
+    abc.set_c(&mut db).to(CycleQuery::A); // force new revision
     assert!(cycle_a(&db, abc).is_err());
 }
 
@@ -276,7 +276,7 @@ fn cycle_appears() {
     //     A --> B
     //     ^     |
     //     +-----+
-    abc.set_b(&mut db, CycleQuery::A);
+    abc.set_b(&mut db).to(CycleQuery::A);
     assert!(cycle_a(&db, abc).is_err());
 }
 
@@ -291,7 +291,7 @@ fn cycle_disappears() {
     assert!(cycle_a(&db, abc).is_err());
 
     //     A --> B
-    abc.set_b(&mut db, CycleQuery::None);
+    abc.set_b(&mut db).to(CycleQuery::None);
     assert!(cycle_a(&db, abc).is_ok());
 }
 
diff --git a/salsa-2022-tests/tests/deletion-cascade.rs b/salsa-2022-tests/tests/deletion-cascade.rs
index d52f698b..2758818e 100644
--- a/salsa-2022-tests/tests/deletion-cascade.rs
+++ b/salsa-2022-tests/tests/deletion-cascade.rs
@@ -117,7 +117,7 @@ fn basic() {
     // * the struct's field
     // * the `copy_field` result
 
-    input.set_field(&mut db, 2);
+    input.set_field(&mut db).to(2);
     assert_eq!(final_result(&db, input), 1 * 2 + 0 * 2);
     db.assert_logs(expect![[r#"
         [
diff --git a/salsa-2022-tests/tests/deletion.rs b/salsa-2022-tests/tests/deletion.rs
index b5c756b4..efb61666 100644
--- a/salsa-2022-tests/tests/deletion.rs
+++ b/salsa-2022-tests/tests/deletion.rs
@@ -103,7 +103,7 @@ fn basic() {
     // * the struct itself
     // * the struct's field
     // * the `contribution_from_struct` result
-    input.set_field(&mut db, 2);
+    input.set_field(&mut db).to(2);
     assert_eq!(final_result(&db, input), 1 * 2 + 0 * 2);
     db.assert_logs(expect![[r#"
         [
diff --git a/salsa-2022-tests/tests/expect_reuse_field_x_of_a_tracked_struct_changes_but_fn_depends_on_field_y.rs b/salsa-2022-tests/tests/expect_reuse_field_x_of_a_tracked_struct_changes_but_fn_depends_on_field_y.rs
index 86cf517c..e4185c7d 100644
--- a/salsa-2022-tests/tests/expect_reuse_field_x_of_a_tracked_struct_changes_but_fn_depends_on_field_y.rs
+++ b/salsa-2022-tests/tests/expect_reuse_field_x_of_a_tracked_struct_changes_but_fn_depends_on_field_y.rs
@@ -91,7 +91,7 @@ fn execute() {
             "final_result_depends_on_y(MyInput(Id { value: 1 }))",
         ]"#]]);
 
-    input.set_field(&mut db, 23);
+    input.set_field(&mut db).to(23);
     // x = (23 + 1) / 2 = 12
     // Intermediate result x changes, so final result depends on x
     // needs to be recomputed;
diff --git a/salsa-2022-tests/tests/expect_reuse_field_x_of_an_input_changes_but_fn_depends_on_field_y.rs b/salsa-2022-tests/tests/expect_reuse_field_x_of_an_input_changes_but_fn_depends_on_field_y.rs
index cb1331c7..5de3b98e 100644
--- a/salsa-2022-tests/tests/expect_reuse_field_x_of_an_input_changes_but_fn_depends_on_field_y.rs
+++ b/salsa-2022-tests/tests/expect_reuse_field_x_of_an_input_changes_but_fn_depends_on_field_y.rs
@@ -70,7 +70,7 @@ fn execute() {
             "result_depends_on_y(MyInput(Id { value: 1 }))",
         ]"#]]);
 
-    input.set_x(&mut db, 23);
+    input.set_x(&mut db).to(23);
     // input x changes, so result depends on x needs to be recomputed;
     assert_eq!(result_depends_on_x(&db, input), 24);
     db.assert_logs(expect![[r#"
diff --git a/salsa-2022-tests/tests/hello_world.rs b/salsa-2022-tests/tests/hello_world.rs
index 2484245e..99d53940 100644
--- a/salsa-2022-tests/tests/hello_world.rs
+++ b/salsa-2022-tests/tests/hello_world.rs
@@ -68,14 +68,14 @@ fn execute() {
 
     // Intermediate result is the same, so final result does
     // not need to be recomputed:
-    input.set_field(&mut db, 23);
+    input.set_field(&mut db).to(23);
     assert_eq!(final_result(&db, input), 22);
     db.assert_logs(expect![[r#"
         [
             "intermediate_result(MyInput(Id { value: 1 }))",
         ]"#]]);
 
-    input.set_field(&mut db, 24);
+    input.set_field(&mut db).to(24);
     assert_eq!(final_result(&db, input), 24);
     db.assert_logs(expect![[r#"
         [
@@ -101,7 +101,7 @@ fn red_herring() {
     // This will trigger a new revision in the database
     // but shouldn't actually invalidate our existing ones.
     let input2 = MyInput::new(&mut db, 44);
-    input2.set_field(&mut db, 66);
+    input2.set_field(&mut db).to(66);
 
     // Re-run the query on the original input. Nothing re-executes!
     assert_eq!(final_result(&db, input), 22);
diff --git a/salsa-2022-tests/tests/mutate_in_place.rs b/salsa-2022-tests/tests/mutate_in_place.rs
index 83166c96..b6330345 100644
--- a/salsa-2022-tests/tests/mutate_in_place.rs
+++ b/salsa-2022-tests/tests/mutate_in_place.rs
@@ -44,12 +44,12 @@ fn execute() {
 
     // Overwrite field with an empty String
     // and store the old value in my_string
-    let mut my_string = input.set_field(&mut db, String::new());
+    let mut my_string = input.set_field(&mut db).to(String::new());
     my_string.push_str(" World!");
 
     // Set the field back to out initial String,
     // expecting to get the empty one back
-    assert_eq!(input.set_field(&mut db, my_string), "");
+    assert_eq!(input.set_field(&mut db).to(my_string), "");
 
     // Check if the stored String is the one we expected
     assert_eq!(input.field(&db), "Hello World!");
diff --git a/salsa-2022-tests/tests/override_new_get_set.rs b/salsa-2022-tests/tests/override_new_get_set.rs
index 86a5190c..80470e67 100644
--- a/salsa-2022-tests/tests/override_new_get_set.rs
+++ b/salsa-2022-tests/tests/override_new_get_set.rs
@@ -27,7 +27,7 @@ impl MyInput {
     }
 
     pub fn set_field(self, db: &mut dyn Db, id: String) {
-        self.set_text(db, id);
+        self.set_text(db).to(id);
     }
 }
 
diff --git a/salsa-2022-tests/tests/specify_tracked_fn_in_rev_1_but_not_2.rs b/salsa-2022-tests/tests/specify_tracked_fn_in_rev_1_but_not_2.rs
index c1ab5003..372624ab 100644
--- a/salsa-2022-tests/tests/specify_tracked_fn_in_rev_1_but_not_2.rs
+++ b/salsa-2022-tests/tests/specify_tracked_fn_in_rev_1_but_not_2.rs
@@ -207,7 +207,7 @@ fn test_run_0_then_5_then_20() {
     //
     // * `create_tracked` does re-execute, but specifies same value for `maybe_specified` as before
     // * `read_maybe_specified` does not re-execute (its input has not changed)
-    input.set_field(&mut db, 5);
+    input.set_field(&mut db).to(5);
     assert_eq!(final_result(&db, input), 100);
     db.assert_logs(expect![[r#"
         [
@@ -226,7 +226,7 @@ fn test_run_0_then_5_then_20() {
     // * `create_tracked` re-executes but does not specify any value
     // * `read_maybe_specified` is invoked and it calls `maybe_specified`, which now executes
     //   (its value has not been specified)
-    input.set_field(&mut db, 20);
+    input.set_field(&mut db).to(20);
     assert_eq!(final_result(&db, input), 200);
     db.assert_logs(expect![[r#"
         [
@@ -278,7 +278,7 @@ fn test_run_0_then_5_then_10_then_20() {
     //
     // * `create_tracked` does re-execute, but specifies same value for `maybe_specified` as before
     // * `read_maybe_specified` does not re-execute (its input has not changed)
-    input.set_field(&mut db, 5);
+    input.set_field(&mut db).to(5);
     assert_eq!(final_result(&db, input), 100);
     db.assert_logs(expect![[r#"
         [
@@ -297,7 +297,7 @@ fn test_run_0_then_5_then_10_then_20() {
     // * `create_tracked` does re-execute and specifies no value for `maybe_specified`
     // * `maybe_specified_value` returns 10; this is the same value as was specified.
     // * `read_maybe_specified` therefore does NOT need to execute.
-    input.set_field(&mut db, 10);
+    input.set_field(&mut db).to(10);
     assert_eq!(final_result(&db, input), 100);
     db.assert_logs(expect![[r#"
         [
@@ -318,7 +318,7 @@ fn test_run_0_then_5_then_10_then_20() {
     // Set input to 20:
     //
     // * Everything re-executes to get new result (200).
-    input.set_field(&mut db, 20);
+    input.set_field(&mut db).to(20);
     assert_eq!(final_result(&db, input), 200);
     db.assert_logs(expect![[r#"
         [
@@ -362,7 +362,7 @@ fn test_run_5_then_20() {
             "Event { runtime_id: RuntimeId { counter: 0 }, kind: WillCheckCancellation }",
         ]"#]]);
 
-    input.set_field(&mut db, 20);
+    input.set_field(&mut db).to(20);
     assert_eq!(final_result(&db, input), 200);
     db.assert_logs(expect![[r#"
         [
diff --git a/salsa-2022-tests/tests/tracked_fn_read_own_entity.rs b/salsa-2022-tests/tests/tracked_fn_read_own_entity.rs
index 949a5427..6d45a9e2 100644
--- a/salsa-2022-tests/tests/tracked_fn_read_own_entity.rs
+++ b/salsa-2022-tests/tests/tracked_fn_read_own_entity.rs
@@ -69,14 +69,14 @@ fn one_entity() {
 
     // Intermediate result is the same, so final result does
     // not need to be recomputed:
-    input.set_field(&mut db, 23);
+    input.set_field(&mut db).to(23);
     assert_eq!(final_result(&db, input), 22);
     db.assert_logs(expect![[r#"
         [
             "intermediate_result(MyInput(Id { value: 1 }))",
         ]"#]]);
 
-    input.set_field(&mut db, 24);
+    input.set_field(&mut db).to(24);
     assert_eq!(final_result(&db, input), 24);
     db.assert_logs(expect![[r#"
         [
@@ -102,7 +102,7 @@ fn red_herring() {
     // This will trigger a new revision in the database
     // but shouldn't actually invalidate our existing ones.
     let input2 = MyInput::new(&mut db, 44);
-    input2.set_field(&mut db, 66);
+    input2.set_field(&mut db).to(66);
 
     // Re-run the query on the original input. Nothing re-executes!
     assert_eq!(final_result(&db, input), 22);