feat: add previous() to access a tracked function's prior memoized value#1219
feat: add previous() to access a tracked function's prior memoized value#1219Veykril wants to merge 2 commits into
previous() to access a tracked function's prior memoized value#1219Conversation
✅ Deploy Preview for salsa-rs canceled.
|
|
@MichaReiser this is the feature i was hinting at (I just revived the branch on top of main). There are still quite a bit of unknowns that I have to think about in terms of whether this is sound and what exactly it means to read your previous memo in a query, but opus thinks its safe (and iirc codex which I used to write this back then agreed). |
|
Yeah, I think this should probably be fine (you already handle fixpoint queries, which was my main concern :)) So this could e.g. be used for an incremental parse? |
|
Also I haven't given the tests a proper look yet (they are llm generated) and there are likely scenarios that are missing still. As for why r-a is interested in this, see rust-lang/rust-analyzer#22532. Basically on trivia edits, we don't want to re-execute the proc-macros, instead we'd like to just take the previous result, fix up the spans manually and emit that as the new memo. So yea, this is useful for more manual incremental recomputation handling. |
Merging this PR will degrade performance by 87.24%
Warning Please fix the performance issues or acknowledge them on CodSpeed. Performance Changes
Tip Investigate this regression by commenting Comparing Footnotes
|
…value Introduce a generated `previous()` associated function on every tracked function, returning a reference to the value memoized for the active key in the previous revision (or `None` if the function has never completed for that key, or its prior result is provisional). This lets a tracked function build its new result on top of its own prior result while re-executing. Reading the previous value replays that result's dependencies into the current execution — durability, dependency edges, untracked-read flag, changed-at revision, and tracked-struct ids — so incremental validation stays correct and previously-created tracked structs remain live.
df38653 to
df88912
Compare
| /// For a specifiable function, the previous value may have been assigned via | ||
| /// `specify` rather than computed by this function; the two cases are | ||
| /// indistinguishable here. |
There was a problem hiding this comment.
Calling out the specify case, we could recognize this and have the function return something else here (enum distinguishing the cases) but I don't think thats worth it until useful to someone.
There was a problem hiding this comment.
I agree with your reasoning here. The idea of specify is that it's mostly transparent. So handling them as described here makes sense to me
|
I think I've covered everything now (forgot about accumulators which is now fixed) |
|
I haven't reviewed the code yet. But I wonder if |
|
Can we just return it or would that require more plumbing? Not familiar with how fixpoint deals with its memos |
|
Let me have a look at the code. I'm not sure what you mean by just returning it :) Overall, the memos are handled mostly the same as regular memos. Except that we replace them after each iteration |
There was a problem hiding this comment.
Probably not what you intended it for, but I have a use case for this in ty to basically check: Has this been computed, if so, call the query, otherwise do something else.
I have to think this through a bit more carefully, but I don't think this is sound for non 'static values. That is, any value that contains anything with a 'db lifetime, because you can now read values as they're been updated or that haven't been verified yet (or GCed).
Another issue is that calling previous multiple times calls seed_tracked_struct multiples times.
| ) -> Option<&$db_lt $output_ty> { | ||
| use ::salsa::plumbing as $zalsa; | ||
|
|
||
| $zalsa::attach($db, || { |
There was a problem hiding this comment.
Do we need to attach here? Is it mainly to assert the same-db constraint?
| /// # Panics | ||
| /// | ||
| /// Panics if the active query is not an execution of this tracked function. | ||
| pub fn previous<'db>( |
There was a problem hiding this comment.
I need to land my monomorphization PR, so that we can move this out of the C block :)
| let revisions = &memo.header.revisions; | ||
| zalsa_local.report_previous_memo_read( | ||
| revisions.durability, | ||
| revisions.origin().edges(), |
There was a problem hiding this comment.
Hmm, I'm not sure this is sound. I have to think this through a bit more carefully.
There's no guarantee that the input edges from a previous revision are still valid. That is, some input edges may no longer exist because they were garbage collected, or were never deserialized after persistent caching.
Carrying these dependencies forward could also mean that a query now depends on the same IDs that only differ b y generations.
| let memo_ingredient_index = self.memo_ingredient_index(zalsa, id); | ||
| let memo = self.get_memo_from_table_for(zalsa, id, memo_ingredient_index)?; | ||
|
|
||
| if memo.header.may_be_provisional() { |
There was a problem hiding this comment.
Maybe it's best to exclude provisional memos for now, even if they're from the previous revision (it's not guaranteed that all memos participating in a cycle are finalized).
For same-revisional provisionals, we'd have to carry over the cycle heads. Ultimately, this is the same as calling self again (fixpoint!). It's a bit less clear to me what needs to happen when depending on a provisional from a previous revision.
| revisions.accumulated_inputs.load(), | ||
| zalsa.current_revision(), | ||
| ); | ||
| Some(value) |
There was a problem hiding this comment.
What's unclear to me is whether this is safe if value contains tracked structs that were created by the active query. You can now read tracked structs that are being updated as your query runs. In general, you can now read values that are potentially outdated, because they haven't been verified yet (or contain interned values that were garbage collected)
|
I think restricting values to It's not even clear to me what the expected dependencies should be. Depending on all previous dependencies is an over-approximation. I also doesn't protect against missing new dependencies. E.g. a query could clone parts of the result and skip a struct read, which it normally would depend on if I sort of think that we may need to iterate over all dependencies and repeat some of
The part that remains unclear to me are tracked structs. But I'd be fine to limit this feature to return To some extend, this reminds me of fixpoint where |
Introduce a generated
previous()associated function on every trackedfunction, returning a reference to the value memoized for the active key
in the previous revision (or
Noneif the function has never completedfor that key, or its prior result is provisional).
This lets a tracked function build its new result on top of its own
prior result while re-executing. Reading the previous value replays that
result's dependencies into the current execution — durability, dependency
edges, untracked-read flag, changed-at revision, and tracked-struct ids —
so incremental validation stays correct and previously-created tracked
structs remain live.