Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions components/salsa-macro-rules/src/setup_tracked_fn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,40 @@ macro_rules! setup_tracked_fn {
}
}

/// Returns a reference to this function's memoized value from the previous
/// revision, or `None` if there is no usable previous value.
///
/// This is only meaningful while the function is re-executing for the current
/// key: it lets the new computation build on its own prior result. Reading the
/// previous value replays that result's dependencies into the current execution,
/// so incremental validation stays correct.
///
/// Returns `None` when the function has never completed for this key, when the
/// previous result is provisional (for example, mid-cycle), or when the memoized
/// value has been evicted (for example, due to an LRU capacity limit).
///
/// Values accumulated by the previous execution itself are not replayed; a
/// function that accumulates must push its values again even when it returns
/// its previous result.
///
/// 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.
Comment on lines +488 to +490

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

///
/// # Panics
///
/// Panics if called outside of this tracked function's own execution.
pub fn previous<$db_lt>(
$db: &$db_lt dyn $Db,
) -> Option<&$db_lt $output_ty> {
use ::salsa::plumbing as $zalsa;

$zalsa::attach($db, || {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to attach here? Is it mainly to assert the same-db constraint?

let (zalsa, zalsa_local) = $db.zalsas();
$Configuration::fn_ingredient_($db, zalsa).previous(zalsa, zalsa_local)
})
}

$zalsa::macro_if! { $is_specifiable =>
pub fn specify<$db_lt>(
$db: &$db_lt dyn $Db,
Expand Down
32 changes: 30 additions & 2 deletions src/active_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ use crate::key::DatabaseKeyIndex;
use crate::runtime::Stamp;
use crate::sync::atomic::AtomicBool;
use crate::tracked_struct::{Disambiguator, DisambiguatorMap, IdentityHash, IdentityMap};
use crate::zalsa_local::{OriginAndExtra, QueryEdge, QueryRevisions, QueryRevisionsExtra};
use crate::zalsa_local::{
OriginAndExtra, QueryEdge, QueryEdges, QueryRevisions, QueryRevisionsExtra,
};
use crate::{
Id,
cycle::{CycleHeads, IterationStamp},
Expand Down Expand Up @@ -72,7 +74,7 @@ impl ActiveQuery {
&mut self,
durability: Durability,
changed_at: Revision,
edges: crate::zalsa_local::QueryEdges<'_>,
edges: QueryEdges<'_>,
untracked_read: bool,
active_tracked_ids: &[(Identity, Id)],
) {
Expand All @@ -93,6 +95,32 @@ impl ActiveQuery {
.seed(active_tracked_ids.iter().map(|(id, _)| id));
}

pub(super) fn add_previous_memo_read(
Comment thread
Veykril marked this conversation as resolved.
&mut self,
durability: Durability,
current_revision: Revision,
edges: QueryEdges<'_>,
untracked_read: bool,
tracked_struct_ids: &[(Identity, Id)],
#[cfg(feature = "accumulator")] accumulated_inputs: InputAccumulatedValues,
) {
self.input_outputs.extend(edges);
self.durability = self.durability.min(durability);
self.changed_at = self.changed_at.max(current_revision);
self.untracked_read |= untracked_read;
#[cfg(feature = "accumulator")]
{
self.accumulated_inputs |= accumulated_inputs;
}

// Mark all tracked structs from the previous revision as active and reserve
// their identities so that newly created structs receive later disambiguators.
self.tracked_struct_ids
.mark_all_active(tracked_struct_ids.iter().copied());
self.disambiguator_map
.seed(tracked_struct_ids.iter().map(|(id, _)| id));
}

pub(super) fn take_cycle_heads(&mut self) -> CycleHeads {
std::mem::take(&mut self.cycle_heads)
}
Expand Down
1 change: 1 addition & 0 deletions src/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ mod fetch;
mod inputs;
mod maybe_changed_after;
mod memo;
mod previous;
mod specify;
mod sync;

Expand Down
55 changes: 55 additions & 0 deletions src/function/previous.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use crate::function::{Configuration, IngredientImpl};
use crate::zalsa::Zalsa;
use crate::zalsa_local::ZalsaLocal;

impl<C> IngredientImpl<C>
where
C: Configuration,
{
/// Returns the value memoized for the active key in the previous revision,
/// replaying that result's dependencies into the active query.
///
/// # Panics
///
/// Panics if the active query is not an execution of this tracked function.
pub fn previous<'db>(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to land my monomorphization PR, so that we can move this out of the C block :)

&'db self,
zalsa: &'db Zalsa,
zalsa_local: &'db ZalsaLocal,
) -> Option<&'db C::Output<'db>> {
let Some((database_key_index, _)) = zalsa_local.active_query() else {
panic!(
"cannot access previous memoized value for {} outside of its tracked function",
C::DEBUG_NAME,
);
};

if database_key_index.ingredient_index() != self.index {
panic!(
"cannot access previous memoized value for {} while executing {database_key_index:?}",
C::DEBUG_NAME,
);
}

let id = database_key_index.key_index();
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() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

return None;
}

let value = memo.value.as_ref()?;
let revisions = &memo.header.revisions;
zalsa_local.report_previous_memo_read(
revisions.durability,
revisions.origin().edges(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

revisions.is_derived_untracked(),
revisions.tracked_struct_ids(),
#[cfg(feature = "accumulator")]
revisions.accumulated_inputs.load(),
zalsa.current_revision(),
);
Some(value)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

}
}
1 change: 1 addition & 0 deletions src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ pub struct StructEntry<'db, C>
where
C: Configuration,
{
#[cfg_attr(not(feature = "salsa_unstable"), expect(dead_code))]
value: &'db Value<C>,
key: DatabaseKeyIndex,
}
Expand Down
1 change: 1 addition & 0 deletions src/table/memo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ impl LazyMemoEntries {
self.as_mut_slice()?.get_mut(index)
}

#[cfg(feature = "salsa_unstable")]
fn iter(&self) -> std::slice::Iter<'_, MemoEntry> {
self.as_slice().unwrap_or_default().iter()
}
Expand Down
1 change: 1 addition & 0 deletions src/tracked_struct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,7 @@ pub struct StructEntry<'db, C>
where
C: Configuration,
{
#[cfg_attr(not(feature = "salsa_unstable"), expect(dead_code))]
value: &'db Value<C>,
key: DatabaseKeyIndex,
}
Expand Down
29 changes: 28 additions & 1 deletion src/zalsa_local.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use thin_vec::ThinVec;
#[cfg(feature = "accumulator")]
use crate::accumulator::{
Accumulator,
accumulated_map::{AccumulatedMap, AtomicInputAccumulatedValues},
accumulated_map::{AccumulatedMap, AtomicInputAccumulatedValues, InputAccumulatedValues},
};
use crate::active_query::{CompletedQuery, DetachedInputOutputs, QueryCompletion, QueryStack};
use crate::cycle::{AtomicIterationStamp, CycleHeads, IterationStamp, empty_cycle_heads};
Expand Down Expand Up @@ -392,6 +392,33 @@ impl ZalsaLocal {
}
}

pub(crate) fn report_previous_memo_read(
&self,
durability: Durability,
edges: QueryEdges<'_>,
untracked_read: bool,
tracked_struct_ids: &[(Identity, Id)],
#[cfg(feature = "accumulator")] accumulated_inputs: InputAccumulatedValues,
current_revision: Revision,
) {
// SAFETY: We do not access the query stack reentrantly.
unsafe {
self.with_query_stack_unchecked_mut(|stack| {
if let Some(top_query) = stack.last_mut() {
top_query.add_previous_memo_read(
durability,
current_revision,
edges,
untracked_read,
tracked_struct_ids,
#[cfg(feature = "accumulator")]
accumulated_inputs,
);
}
})
}
}

/// Register that the current query read an untracked value
///
/// # Parameters
Expand Down
Loading