Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] fixpoint iteration support #603

Open
wants to merge 28 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
207656a
add example test for fixpoint iteration
carljm Aug 28, 2024
6d065ca
add a multi-symbol test
carljm Aug 28, 2024
915b245
simplify test case
carljm Sep 21, 2024
fd454f7
WIP: remove existing cycle handling tests for now
carljm Oct 8, 2024
fe1de3c
WIP: remove all existing cycle handling, add fixpoint options
carljm Oct 9, 2024
08901b2
WIP: added provisional value and cycle fields
carljm Oct 12, 2024
abb6fdd
rename to CycleRecoveryStrategy::Fixpoint
carljm Oct 17, 2024
8dc4a0b
WIP: rip out ProvisionalValue
carljm Oct 23, 2024
ff455fc
WIP: working single-iteration with provisional memo
carljm Oct 23, 2024
cd01235
WIP: add count arg to cycle_recovery_fn
carljm Oct 23, 2024
79a1ada
WIP: move insert-initial out into fetch_cold
carljm Oct 23, 2024
7b6b1a7
WIP: cycle-head iteration
carljm Oct 23, 2024
eb0325b
WIP: move loop into execute
carljm Oct 23, 2024
c6c972c
WIP: delay storing memo
carljm Oct 23, 2024
7a425f7
WIP: remove ourself from cycle heads when done iterating
carljm Oct 23, 2024
f84ef80
WIP: working convergence and fallback
carljm Oct 23, 2024
05a7d5b
WIP: clippy and cleanup
carljm Oct 23, 2024
b816aa0
WIP: improve comments and add a type annotation
carljm Oct 24, 2024
7fcfd7f
WIP: don't allow cycle_fn with no_eq
carljm Oct 24, 2024
f411766
WIP: add tracing for cycle iteration
carljm Oct 24, 2024
e16055d
WIP: fail fast if we get an evicted provisional value
carljm Oct 24, 2024
446d655
WIP: use FxHashSet::from_iter
carljm Oct 24, 2024
3325861
add tests, fix multiple-revision, lazy provisional value
carljm Oct 30, 2024
ce1e57b
review feedback, more tracing
carljm Oct 31, 2024
b114580
fix multi-revision bug
carljm Oct 31, 2024
0b59209
better fix for multi-revision bug
carljm Oct 31, 2024
706d0ad
test fixes
carljm Oct 31, 2024
c1bbdcf
pass inputs to cycle recovery functions
carljm Nov 1, 2024
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
19 changes: 13 additions & 6 deletions components/salsa-macro-rules/src/setup_tracked_fn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ macro_rules! setup_tracked_fn {
// Path to the cycle recovery function to use.
cycle_recovery_fn: ($($cycle_recovery_fn:tt)*),

// Path to function to get the initial value to use for cycle recovery.
cycle_recovery_initial: ($($cycle_recovery_initial:tt)*),

// Name of cycle recovery strategy variant to use.
cycle_recovery_strategy: $cycle_recovery_strategy:ident,

Expand Down Expand Up @@ -160,15 +163,15 @@ macro_rules! setup_tracked_fn {

const CYCLE_STRATEGY: $zalsa::CycleRecoveryStrategy = $zalsa::CycleRecoveryStrategy::$cycle_recovery_strategy;

fn should_backdate_value(
fn values_equal(
old_value: &Self::Output<'_>,
new_value: &Self::Output<'_>,
) -> bool {
$zalsa::macro_if! {
if $no_eq {
false
} else {
$zalsa::should_backdate_value(old_value, new_value)
$zalsa::values_equal(old_value, new_value)
}
}
}
Expand All @@ -179,12 +182,16 @@ macro_rules! setup_tracked_fn {
$inner($db, $($input_id),*)
}

fn cycle_initial<$db_lt>(db: &$db_lt dyn $Db) -> Self::Output<$db_lt> {
$($cycle_recovery_initial)*(db)
Copy link
Contributor

Choose a reason for hiding this comment

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

I suspect that it's possible that the initial value function or the recover functions itself could create new cycles. Is this indeed the case and if so, what's salsa's behavior?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm. In the use cases I have in mind (e.g. for red-knot) there should not be any reason to call a query from within one of these functions. And it seems like this could be quite difficult to deal with. So unless we have clear use cases, I would rather consider this out of scope. We could just document "don't do that," or we could add some kind of explicit prevention.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah. I'm not suggesting that users should do that. I only wonder about what happens if a user does it regardless.

Ideally, we wouldn't provide a Db but we can't do that because a user might want to create a tracked struct.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think the simplest approach here would be to add some state on Runtime that causes an error if you try to push a new active query. I'll wait for Niko review before doing that, though.

}

fn recover_from_cycle<$db_lt>(
db: &$db_lt dyn $Db,
cycle: &$zalsa::Cycle,
($($input_id),*): ($($input_ty),*)
) -> Self::Output<$db_lt> {
$($cycle_recovery_fn)*(db, cycle, $($input_id),*)
value: &Self::Output<$db_lt>,
count: u32,
) -> $zalsa::CycleRecoveryAction<Self::Output<$db_lt>> {
$($cycle_recovery_fn)*(db, value, count)
}

fn id_to_input<$db_lt>(db: &$db_lt Self::DbView, key: salsa::Id) -> Self::Input<$db_lt> {
Expand Down
23 changes: 14 additions & 9 deletions components/salsa-macro-rules/src/unexpected_cycle_recovery.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
// Macro that generates the body of the cycle recovery function
// for the case where no cycle recovery is possible. This has to be
// a macro because it can take a variadic number of arguments.
// for the case where no cycle recovery is possible. Must be a macro
// because the signature types must match the particular tracked function.
#[macro_export]
macro_rules! unexpected_cycle_recovery {
($db:ident, $cycle:ident, $($other_inputs:ident),*) => {
{
std::mem::drop($db);
std::mem::drop(($($other_inputs),*));
panic!("cannot recover from cycle `{:?}`", $cycle)
}
}
($db:ident, $value:ident, $count:ident) => {{
std::mem::drop($db);
panic!("cannot recover from cycle")
}};
}

#[macro_export]
macro_rules! unexpected_cycle_initial {
($db:ident) => {{
std::mem::drop($db);
panic!("no cycle initial value")
}};
}
3 changes: 2 additions & 1 deletion components/salsa-macros/src/accumulator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ impl AllowedOptions for Accumulator {
const SINGLETON: bool = false;
const DATA: bool = false;
const DB: bool = false;
const RECOVERY_FN: bool = false;
const CYCLE_FN: bool = false;
const CYCLE_INITIAL: bool = false;
const LRU: bool = false;
const CONSTRUCTOR_NAME: bool = false;
}
Expand Down
4 changes: 3 additions & 1 deletion components/salsa-macros/src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ impl crate::options::AllowedOptions for InputStruct {

const DB: bool = false;

const RECOVERY_FN: bool = false;
const CYCLE_FN: bool = false;

const CYCLE_INITIAL: bool = false;

const LRU: bool = false;

Expand Down
4 changes: 3 additions & 1 deletion components/salsa-macros/src/interned.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ impl crate::options::AllowedOptions for InternedStruct {

const DB: bool = false;

const RECOVERY_FN: bool = false;
const CYCLE_FN: bool = false;

const CYCLE_INITIAL: bool = false;

const LRU: bool = false;

Expand Down
44 changes: 35 additions & 9 deletions components/salsa-macros/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,15 @@ pub(crate) struct Options<A: AllowedOptions> {
/// If this is `Some`, the value is the `<path>`.
pub db_path: Option<syn::Path>,

/// The `recovery_fn = <path>` option is used to indicate the recovery function.
/// The `cycle_fn = <path>` option is used to indicate the cycle recovery function.
///
/// If this is `Some`, the value is the `<path>`.
pub recovery_fn: Option<syn::Path>,
pub cycle_fn: Option<syn::Path>,

/// The `cycle_initial = <path>` option is the initial value for cycle iteration.
///
/// If this is `Some`, the value is the `<path>`.
pub cycle_initial: Option<syn::Path>,

/// The `data = <ident>` option is used to define the name of the data type for an interned
/// struct.
Expand Down Expand Up @@ -79,7 +84,8 @@ impl<A: AllowedOptions> Default for Options<A> {
no_debug: Default::default(),
no_clone: Default::default(),
db_path: Default::default(),
recovery_fn: Default::default(),
cycle_fn: Default::default(),
cycle_initial: Default::default(),
data: Default::default(),
constructor_name: Default::default(),
phantom: Default::default(),
Expand All @@ -99,7 +105,8 @@ pub(crate) trait AllowedOptions {
const SINGLETON: bool;
const DATA: bool;
const DB: bool;
const RECOVERY_FN: bool;
const CYCLE_FN: bool;
const CYCLE_INITIAL: bool;
const LRU: bool;
const CONSTRUCTOR_NAME: bool;
}
Expand Down Expand Up @@ -207,20 +214,39 @@ impl<A: AllowedOptions> syn::parse::Parse for Options<A> {
"`db` option not allowed here",
));
}
} else if ident == "recovery_fn" {
if A::RECOVERY_FN {
} else if ident == "cycle_fn" {
if A::CYCLE_FN {
let _eq = Equals::parse(input)?;
let path = syn::Path::parse(input)?;
if let Some(old) = std::mem::replace(&mut options.cycle_fn, Some(path)) {
return Err(syn::Error::new(
old.span(),
"option `cycle_fn` provided twice",
));
}
} else {
return Err(syn::Error::new(
ident.span(),
"`cycle_fn` option not allowed here",
));
}
} else if ident == "cycle_initial" {
if A::CYCLE_INITIAL {
// TODO(carljm) should it be an error to give cycle_initial without cycle_fn,
// or should we just allow this to fall into potentially infinite iteration, if
// iteration never converges?
let _eq = Equals::parse(input)?;
let path = syn::Path::parse(input)?;
if let Some(old) = std::mem::replace(&mut options.recovery_fn, Some(path)) {
if let Some(old) = std::mem::replace(&mut options.cycle_initial, Some(path)) {
return Err(syn::Error::new(
old.span(),
"option `recovery_fn` provided twice",
"option `cycle_initial` provided twice",
));
}
} else {
return Err(syn::Error::new(
ident.span(),
"`recovery_fn` option not allowed here",
"`cycle_initial` option not allowed here",
));
}
} else if ident == "data" {
Expand Down
44 changes: 35 additions & 9 deletions components/salsa-macros/src/tracked_fn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ impl crate::options::AllowedOptions for TrackedFn {

const DB: bool = false;

const RECOVERY_FN: bool = true;
const CYCLE_FN: bool = true;

const CYCLE_INITIAL: bool = true;

const LRU: bool = true;

Expand Down Expand Up @@ -68,9 +70,20 @@ impl Macro {
let input_ids = self.input_ids(&item);
let input_tys = self.input_tys(&item)?;
let output_ty = self.output_ty(&db_lt, &item)?;
let (cycle_recovery_fn, cycle_recovery_strategy) = self.cycle_recovery();
let (cycle_recovery_fn, cycle_recovery_initial, cycle_recovery_strategy) =
self.cycle_recovery()?;
let is_specifiable = self.args.specify.is_some();
let no_eq = self.args.no_eq.is_some();
let no_eq = if let Some(token) = &self.args.no_eq {
if self.args.cycle_fn.is_some() {
return Err(syn::Error::new_spanned(
token,
"the `no_eq` option cannot be used with `cycle_fn`",
));
}
true
} else {
false
};

let mut inner_fn = item.clone();
inner_fn.vis = syn::Visibility::Inherited;
Expand Down Expand Up @@ -127,6 +140,7 @@ impl Macro {
output_ty: #output_ty,
inner_fn: #inner_fn,
cycle_recovery_fn: #cycle_recovery_fn,
cycle_recovery_initial: #cycle_recovery_initial,
cycle_recovery_strategy: #cycle_recovery_strategy,
is_specifiable: #is_specifiable,
no_eq: #no_eq,
Expand Down Expand Up @@ -160,14 +174,26 @@ impl Macro {

Ok(ValidFn { db_ident, db_path })
}
fn cycle_recovery(&self) -> (TokenStream, TokenStream) {
if let Some(recovery_fn) = &self.args.recovery_fn {
(quote!((#recovery_fn)), quote!(Fallback))
} else {
(
fn cycle_recovery(&self) -> syn::Result<(TokenStream, TokenStream, TokenStream)> {
match (&self.args.cycle_fn, &self.args.cycle_initial) {
(Some(cycle_fn), Some(cycle_initial)) => Ok((
quote!((#cycle_fn)),
quote!((#cycle_initial)),
quote!(Fixpoint),
)),
(None, None) => Ok((
quote!((salsa::plumbing::unexpected_cycle_recovery!)),
quote!((salsa::plumbing::unexpected_cycle_initial!)),
quote!(Panic),
)
)),
(Some(_), None) => Err(syn::Error::new_spanned(
self.args.cycle_fn.as_ref().unwrap(),
"must provide `cycle_initial` along with `cycle_fn`",
)),
(None, Some(_)) => Err(syn::Error::new_spanned(
self.args.cycle_initial.as_ref().unwrap(),
"must provide `cycle_fn` along with `cycle_initial`",
)),
}
}

Expand Down
4 changes: 3 additions & 1 deletion components/salsa-macros/src/tracked_struct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ impl crate::options::AllowedOptions for TrackedStruct {

const DB: bool = false;

const RECOVERY_FN: bool = false;
const CYCLE_FN: bool = false;

const CYCLE_INITIAL: bool = false;

const LRU: bool = false;

Expand Down
43 changes: 10 additions & 33 deletions src/active_query.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use rustc_hash::FxHashMap;
use rustc_hash::{FxHashMap, FxHashSet};

use super::zalsa_local::{EdgeKind, QueryEdges, QueryOrigin, QueryRevisions};
use crate::tracked_struct::IdentityHash;
Expand All @@ -9,7 +9,7 @@ use crate::{
key::{DatabaseKeyIndex, DependencyIndex},
tracked_struct::{Disambiguator, Identity},
zalsa_local::EMPTY_DEPENDENCIES,
Cycle, Id, Revision,
Id, Revision,
};

#[derive(Debug)]
Expand All @@ -35,9 +35,6 @@ pub(crate) struct ActiveQuery {
/// True if there was an untracked read.
untracked_read: bool,

/// Stores the entire cycle, if one is found and this query is part of it.
pub(crate) cycle: Option<Cycle>,

/// When new tracked structs are created, their data is hashed, and the resulting
/// hash is added to this map. If it is not present, then the disambiguator is 0.
/// Otherwise it is 1 more than the current value (which is incremented).
Expand All @@ -54,6 +51,9 @@ pub(crate) struct ActiveQuery {
/// Stores the values accumulated to the given ingredient.
/// The type of accumulated value is erased but known to the ingredient.
pub(crate) accumulated: AccumulatedMap,

/// Provisional cycle results that this query depends on.
pub(crate) cycle_heads: FxHashSet<DatabaseKeyIndex>,
}

impl ActiveQuery {
Expand All @@ -64,10 +64,10 @@ impl ActiveQuery {
changed_at: Revision::start(),
input_outputs: FxIndexSet::default(),
untracked_read: false,
cycle: None,
disambiguator_map: Default::default(),
tracked_struct_ids: Default::default(),
accumulated: Default::default(),
cycle_heads: Default::default(),
}
}

Expand All @@ -76,10 +76,12 @@ impl ActiveQuery {
input: DependencyIndex,
durability: Durability,
revision: Revision,
cycle_heads: &FxHashSet<DatabaseKeyIndex>,
) {
self.input_outputs.insert((EdgeKind::Input, input));
self.durability = self.durability.min(durability);
self.changed_at = self.changed_at.max(revision);
self.cycle_heads.extend(cycle_heads);
}

pub(super) fn add_untracked_read(&mut self, changed_at: Revision) {
Expand Down Expand Up @@ -125,36 +127,11 @@ impl ActiveQuery {
durability: self.durability,
tracked_struct_ids: self.tracked_struct_ids,
accumulated: self.accumulated,
cycle_ignore: !self.cycle_heads.is_empty(),
cycle_heads: self.cycle_heads,
}
}

/// Adds any dependencies from `other` into `self`.
/// Used during cycle recovery, see [`Runtime::unblock_cycle_and_maybe_throw`].
pub(super) fn add_from(&mut self, other: &ActiveQuery) {
self.changed_at = self.changed_at.max(other.changed_at);
self.durability = self.durability.min(other.durability);
self.untracked_read |= other.untracked_read;
self.input_outputs
.extend(other.input_outputs.iter().copied());
}

/// Removes the participants in `cycle` from my dependencies.
/// Used during cycle recovery, see [`Runtime::unblock_cycle_and_maybe_throw`].
pub(super) fn remove_cycle_participants(&mut self, cycle: &Cycle) {
for p in cycle.participant_keys() {
let p: DependencyIndex = p.into();
self.input_outputs.shift_remove(&(EdgeKind::Input, p));
}
}

/// Copy the changed-at, durability, and dependencies from `cycle_query`.
/// Used during cycle recovery, see [`Runtime::unblock_cycle_and_maybe_throw`].
pub(crate) fn take_inputs_from(&mut self, cycle_query: &ActiveQuery) {
self.changed_at = cycle_query.changed_at;
self.durability = cycle_query.durability;
self.input_outputs.clone_from(&cycle_query.input_outputs);
}

pub(super) fn disambiguate(&mut self, key: IdentityHash) -> Disambiguator {
let disambiguator = self
.disambiguator_map
Expand Down
Loading
Loading