diff --git a/book/src/cycles.md b/book/src/cycles.md index 8222d9eaf..fb3ac5460 100644 --- a/book/src/cycles.md +++ b/book/src/cycles.md @@ -2,7 +2,11 @@ By default, when Salsa detects a cycle in the computation graph, Salsa will panic with a message naming the "cycle head"; this is the query that was called while it was also on the active query stack, creating a cycle. -Salsa also supports recovering from query cycles via fixed-point iteration. Fixed-point iteration is only usable if the queries which may be involved in a cycle are monotone and operate on a value domain which is a partial order with fixed height. Effectively, this means that the queries' output must always be "larger" than its input, and there must be some "maximum" or "top" value. This ensures that fixed-point iteration will converge to a value. (A typical case would be queries operating on types, which form a partial order with a "top" type.) +Salsa supports three recovery modes: panicking (the default), fixpoint resolution and immediate fallback. + +## Fixpoint Resolution + +Fixed-point iteration is only usable if the queries which may be involved in a cycle are monotone and operate on a value domain which is a partial order with fixed height. Effectively, this means that the queries' output must always be "larger" than its input, and there must be some "maximum" or "top" value. This ensures that fixed-point iteration will converge to a value. (A typical case would be queries operating on types, which form a partial order with a "top" type.) In order to support fixed-point iteration for a query, provide the `cycle_fn` and `cycle_initial` arguments to `salsa::tracked`: @@ -27,14 +31,38 @@ If the `cycle_fn` continues to return `Iterate`, the cycle will iterate until it If the `cycle_fn` returns `Fallback`, the cycle will iterate one last time and verify that the returned value is the same as the fallback value; that is, the fallback value results in a stable converged cycle. If not, Salsa will panic. It is not permitted to use a fallback value that does not converge, because this would leave the cycle in an unpredictable state, depending on the order of query execution. -## All potential cycle heads must set `cycle_fn` and `cycle_initial` +### All potential cycle heads must set `cycle_fn` and `cycle_initial` Consider a two-query cycle where `query_a` calls `query_b`, and `query_b` calls `query_a`. If `query_a` is called first, then it will become the "cycle head", but if `query_b` is called first, then `query_b` will be the cycle head. In order for a cycle to use fixed-point iteration instead of panicking, the cycle head must set `cycle_fn` and `cycle_initial`. This means that in order to be robust against varying query execution order, both `query_a` and `query_b` must set `cycle_fn` and `cycle_initial`. -## Ensuring convergence +### Ensuring convergence Fixed-point iteration is a powerful tool, but is also easy to misuse, potentially resulting in infinite iteration. To avoid this, ensure that all queries participating in fixpoint iteration are deterministic and monotone. -## Calling Salsa queries from within `cycle_fn` or `cycle_initial` +### Calling Salsa queries from within `cycle_fn` or `cycle_initial` It is permitted to call other Salsa queries from within the `cycle_fn` and `cycle_initial` functions. However, if these functions re-enter the same cycle, this can lead to unpredictable results. Take care which queries are called from within cycle-recovery functions, and avoid triggering further cycles. + +## Immediate Fallback + +This mode of cycle handling causes query calls that result in a cycle to immediately return with a fallback value. + +In order to support this fallback for a query, provide the `cycle_result` argument to `salsa::tracked`: + +```rust +#[salsa::tracked(cycle_result=fallback)] +fn query(db: &dyn salsa::Database) -> u32 { + // ... +} + +fn fallback(_db: &dyn KnobsDatabase) -> u32 { + 0 +} +``` + +### Observable execution order + +One problem with this fallback mode is that execution order / entry points become part of the query computation and can affect the results of queries containing cycles. +This introduces a potential form on non-determinism depending on the query graph when multiple differing cycling queries are involved. +Due to this, when an immediate fallback cycle occurs, salsa walks back the active query stacks to verify that the cycle does not occur within the context of another non-panic cycle query. +In other words, it is only valid to immediate fallback cycle recover for a query if either all ancestors queries are panic cycle queries or if the cycle is immediate self-referential. diff --git a/components/salsa-macros/src/accumulator.rs b/components/salsa-macros/src/accumulator.rs index 926fdb547..220d0e941 100644 --- a/components/salsa-macros/src/accumulator.rs +++ b/components/salsa-macros/src/accumulator.rs @@ -40,6 +40,7 @@ impl AllowedOptions for Accumulator { const DB: bool = false; const CYCLE_FN: bool = false; const CYCLE_INITIAL: bool = false; + const CYCLE_RESULT: bool = false; const LRU: bool = false; const CONSTRUCTOR_NAME: bool = false; const ID: bool = false; diff --git a/components/salsa-macros/src/input.rs b/components/salsa-macros/src/input.rs index 0e5f92c7c..38241a959 100644 --- a/components/salsa-macros/src/input.rs +++ b/components/salsa-macros/src/input.rs @@ -55,6 +55,8 @@ impl crate::options::AllowedOptions for InputStruct { const CYCLE_INITIAL: bool = false; + const CYCLE_RESULT: bool = false; + const LRU: bool = false; const CONSTRUCTOR_NAME: bool = true; diff --git a/components/salsa-macros/src/interned.rs b/components/salsa-macros/src/interned.rs index 7067d3010..646470fce 100644 --- a/components/salsa-macros/src/interned.rs +++ b/components/salsa-macros/src/interned.rs @@ -55,6 +55,8 @@ impl crate::options::AllowedOptions for InternedStruct { const CYCLE_INITIAL: bool = false; + const CYCLE_RESULT: bool = false; + const LRU: bool = false; const CONSTRUCTOR_NAME: bool = true; diff --git a/components/salsa-macros/src/options.rs b/components/salsa-macros/src/options.rs index ded09017e..e69e70a12 100644 --- a/components/salsa-macros/src/options.rs +++ b/components/salsa-macros/src/options.rs @@ -61,6 +61,11 @@ pub(crate) struct Options { /// If this is `Some`, the value is the ``. pub cycle_initial: Option, + /// The `cycle_result = ` option is the result for non-fixpoint cycle. + /// + /// If this is `Some`, the value is the ``. + pub cycle_result: Option, + /// The `data = ` option is used to define the name of the data type for an interned /// struct. /// @@ -100,6 +105,7 @@ impl Default for Options { db_path: Default::default(), cycle_fn: Default::default(), cycle_initial: Default::default(), + cycle_result: Default::default(), data: Default::default(), constructor_name: Default::default(), phantom: Default::default(), @@ -123,6 +129,7 @@ pub(crate) trait AllowedOptions { const DB: bool; const CYCLE_FN: bool; const CYCLE_INITIAL: bool; + const CYCLE_RESULT: bool; const LRU: bool; const CONSTRUCTOR_NAME: bool; const ID: bool; @@ -274,6 +281,22 @@ impl syn::parse::Parse for Options { "`cycle_initial` option not allowed here", )); } + } else if ident == "cycle_result" { + if A::CYCLE_RESULT { + let _eq = Equals::parse(input)?; + let expr = syn::Expr::parse(input)?; + if let Some(old) = options.cycle_result.replace(expr) { + return Err(syn::Error::new( + old.span(), + "option `cycle_result` provided twice", + )); + } + } else { + return Err(syn::Error::new( + ident.span(), + "`cycle_result` option not allowed here", + )); + } } else if ident == "data" { if A::DATA { let _eq = Equals::parse(input)?; diff --git a/components/salsa-macros/src/tracked_fn.rs b/components/salsa-macros/src/tracked_fn.rs index 66f1fc82c..67cc3efab 100644 --- a/components/salsa-macros/src/tracked_fn.rs +++ b/components/salsa-macros/src/tracked_fn.rs @@ -48,6 +48,8 @@ impl crate::options::AllowedOptions for TrackedFn { const CYCLE_INITIAL: bool = true; + const CYCLE_RESULT: bool = true; + const LRU: bool = true; const CONSTRUCTOR_NAME: bool = false; @@ -201,25 +203,38 @@ impl Macro { fn cycle_recovery(&self) -> syn::Result<(TokenStream, TokenStream, TokenStream)> { // TODO should we ask the user to specify a struct that impls a trait with two methods, // rather than asking for two methods separately? - match (&self.args.cycle_fn, &self.args.cycle_initial) { - (Some(cycle_fn), Some(cycle_initial)) => Ok(( + match ( + &self.args.cycle_fn, + &self.args.cycle_initial, + &self.args.cycle_result, + ) { + (Some(cycle_fn), Some(cycle_initial), None) => Ok(( quote!((#cycle_fn)), quote!((#cycle_initial)), quote!(Fixpoint), )), - (None, None) => Ok(( + (None, None, None) => Ok(( quote!((salsa::plumbing::unexpected_cycle_recovery!)), quote!((salsa::plumbing::unexpected_cycle_initial!)), quote!(Panic), )), - (Some(_), None) => Err(syn::Error::new_spanned( + (Some(_), None, 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( + (None, Some(_), None) => Err(syn::Error::new_spanned( self.args.cycle_initial.as_ref().unwrap(), "must provide `cycle_fn` along with `cycle_initial`", )), + (None, None, Some(cycle_result)) => Ok(( + quote!((salsa::plumbing::unexpected_cycle_recovery!)), + quote!((#cycle_result)), + quote!(FallbackImmediate), + )), + (_, _, Some(_)) => Err(syn::Error::new_spanned( + self.args.cycle_initial.as_ref().unwrap(), + "must provide either both `cycle_fn` and `cycle_initial`, or only `cycle_result`", + )), } } diff --git a/components/salsa-macros/src/tracked_struct.rs b/components/salsa-macros/src/tracked_struct.rs index d72b2ae78..f2a4ab9ab 100644 --- a/components/salsa-macros/src/tracked_struct.rs +++ b/components/salsa-macros/src/tracked_struct.rs @@ -50,6 +50,8 @@ impl crate::options::AllowedOptions for TrackedStruct { const CYCLE_INITIAL: bool = false; + const CYCLE_RESULT: bool = false; + const LRU: bool = false; const CONSTRUCTOR_NAME: bool = true; diff --git a/src/active_query.rs b/src/active_query.rs index 0211b1df0..2699d5901 100644 --- a/src/active_query.rs +++ b/src/active_query.rs @@ -5,7 +5,7 @@ use std::{mem, ops}; use crate::accumulator::accumulated_map::{ AccumulatedMap, AtomicInputAccumulatedValues, InputAccumulatedValues, }; -use crate::cycle::CycleHeads; +use crate::cycle::{CycleHeads, CycleRecoveryStrategy}; use crate::durability::Durability; use crate::hash::FxIndexSet; use crate::key::DatabaseKeyIndex; @@ -63,6 +63,9 @@ pub(crate) struct ActiveQuery { /// If this query is a cycle head, iteration count of that cycle. iteration_count: u32, + + /// The cycle strategy for this query. + cycle_strategy: CycleRecoveryStrategy, } impl ActiveQuery { @@ -137,10 +140,18 @@ impl ActiveQuery { pub(super) fn iteration_count(&self) -> u32 { self.iteration_count } + + pub(crate) fn cycle_strategy(&self) -> CycleRecoveryStrategy { + self.cycle_strategy + } } impl ActiveQuery { - fn new(database_key_index: DatabaseKeyIndex, iteration_count: u32) -> Self { + fn new( + database_key_index: DatabaseKeyIndex, + iteration_count: u32, + cycle_strategy: CycleRecoveryStrategy, + ) -> Self { ActiveQuery { database_key_index, durability: Durability::MAX, @@ -153,6 +164,7 @@ impl ActiveQuery { accumulated_inputs: Default::default(), cycle_heads: Default::default(), iteration_count, + cycle_strategy, } } @@ -169,6 +181,7 @@ impl ActiveQuery { accumulated_inputs, ref mut cycle_heads, iteration_count: _, + cycle_strategy: _, } = self; let edges = QueryEdges::new(input_outputs.drain(..)); @@ -210,6 +223,7 @@ impl ActiveQuery { accumulated_inputs: _, cycle_heads, iteration_count, + cycle_strategy: _, } = self; input_outputs.clear(); disambiguator_map.clear(); @@ -219,7 +233,12 @@ impl ActiveQuery { *iteration_count = 0; } - fn reset_for(&mut self, new_database_key_index: DatabaseKeyIndex, new_iteration_count: u32) { + fn reset_for( + &mut self, + new_database_key_index: DatabaseKeyIndex, + new_iteration_count: u32, + new_cycle_strategy: CycleRecoveryStrategy, + ) { let Self { database_key_index, durability, @@ -232,6 +251,7 @@ impl ActiveQuery { accumulated_inputs, cycle_heads, iteration_count, + cycle_strategy: is_cycle_result_query, } = self; *database_key_index = new_database_key_index; *durability = Durability::MAX; @@ -239,6 +259,7 @@ impl ActiveQuery { *untracked_read = false; *accumulated_inputs = Default::default(); *iteration_count = new_iteration_count; + *is_cycle_result_query = new_cycle_strategy; debug_assert!( input_outputs.is_empty(), "`ActiveQuery::clear` or `ActiveQuery::into_revisions` should've been called" @@ -304,12 +325,16 @@ impl QueryStack { &mut self, database_key_index: DatabaseKeyIndex, iteration_count: u32, + cycle_strategy: CycleRecoveryStrategy, ) { if self.len < self.stack.len() { - self.stack[self.len].reset_for(database_key_index, iteration_count); + self.stack[self.len].reset_for(database_key_index, iteration_count, cycle_strategy); } else { - self.stack - .push(ActiveQuery::new(database_key_index, iteration_count)); + self.stack.push(ActiveQuery::new( + database_key_index, + iteration_count, + cycle_strategy, + )); } self.len += 1; } diff --git a/src/cycle.rs b/src/cycle.rs index e1c63d653..aa7f5a6de 100644 --- a/src/cycle.rs +++ b/src/cycle.rs @@ -83,6 +83,10 @@ pub enum CycleRecoveryStrategy { /// This choice is computed by the query's `cycle_recovery` /// function and initial value. Fixpoint, + + /// Recovers from cycles by falling back to a sentinel value + /// for cycle invocations. + FallbackImmediate, } /// A "cycle head" is the query at which we encounter a cycle; that is, if A -> B -> C -> A, then A diff --git a/src/function/execute.rs b/src/function/execute.rs index 557463817..f9670e888 100644 --- a/src/function/execute.rs +++ b/src/function/execute.rs @@ -72,7 +72,7 @@ where // initial provisional value from there. let memo = self.get_memo_from_table_for(zalsa, id, memo_ingredient_index) - .unwrap_or_else(|| panic!("{database_key_index:#?} is a cycle head, but no provisional memo found")); + .unwrap_or_else(|| unreachable!("{database_key_index:#?} is a cycle head, but no provisional memo found")); debug_assert!(memo.may_be_provisional()); memo.value.as_ref() }; @@ -140,9 +140,11 @@ where memo_ingredient_index, )); - active_query = db - .zalsa_local() - .push_query(database_key_index, iteration_count); + active_query = db.zalsa_local().push_query( + database_key_index, + iteration_count, + C::CYCLE_STRATEGY, + ); continue; } diff --git a/src/function/fetch.rs b/src/function/fetch.rs index 3d4b389c0..2fefc7b57 100644 --- a/src/function/fetch.rs +++ b/src/function/fetch.rs @@ -1,3 +1,4 @@ +use crate::cycle::CycleRecoveryStrategy; use crate::function::memo::Memo; use crate::function::{Configuration, IngredientImpl, VerifyResult}; use crate::table::sync::ClaimResult; @@ -121,42 +122,43 @@ where shallow_update, ); // SAFETY: memo is present in memo_map. - return unsafe { Some(self.extend_memo_lifetime(memo)) }; + return Some(unsafe { self.extend_memo_lifetime(memo) }); } } } + + let initial_value = match C::CYCLE_STRATEGY { + CycleRecoveryStrategy::Fixpoint => { + C::cycle_initial(db, C::id_to_input(db, database_key_index.key_index())) + } + CycleRecoveryStrategy::FallbackImmediate => { + db.zalsa_local() + .assert_top_non_panic_cycle(database_key_index); + C::cycle_initial(db, C::id_to_input(db, database_key_index.key_index())) + } + CycleRecoveryStrategy::Panic => { + db.zalsa_local().cycle_panic(database_key_index, "querying") + } + }; + + tracing::debug!( + "hit cycle at {database_key_index:#?}, \ + inserting and returning fixpoint initial value" + ); // no provisional value; create/insert/return initial provisional value - return self - .initial_value(db, database_key_index.key_index()) - .map(|initial_value| { - tracing::debug!( - "hit cycle at {database_key_index:#?}, \ - inserting and returning fixpoint initial value" - ); - self.insert_memo( - zalsa, - id, - Memo::new( - Some(initial_value), - zalsa.current_revision(), - QueryRevisions::fixpoint_initial( - database_key_index, - zalsa.current_revision(), - ), - ), - memo_ingredient_index, - ) - }) - .or_else(|| { - db.zalsa_local().with_query_stack(|stack| { - panic!( - "dependency graph cycle when querying {database_key_index:#?}, \ - set cycle_fn/cycle_initial to fixpoint iterate.\n\ - Query stack:\n{:#?}", - stack, - ); - }) - }); + return Some(self.insert_memo( + zalsa, + id, + Memo::new( + Some(initial_value), + zalsa.current_revision(), + QueryRevisions::fixpoint_initial( + database_key_index, + zalsa.current_revision(), + ), + ), + memo_ingredient_index, + )); } ClaimResult::Claimed(guard) => guard, }; @@ -179,7 +181,11 @@ where } } - let memo = self.execute(db, active_query.into_inner(db.zalsa_local()), opt_old_memo); + let memo = self.execute( + db, + active_query.into_inner(db.zalsa_local(), C::CYCLE_STRATEGY), + opt_old_memo, + ); Some(memo) } @@ -202,13 +208,24 @@ impl<'me> LazyActiveQueryGuard<'me> { self.database_key_index } - pub(super) fn guard(&mut self, zalsa_local: &'me ZalsaLocal) -> &ActiveQueryGuard<'me> { - self.guard - .get_or_insert_with(|| zalsa_local.push_query(self.database_key_index, 0)) + #[inline] + pub(super) fn guard( + &mut self, + zalsa_local: &'me ZalsaLocal, + cycle_strategy: CycleRecoveryStrategy, + ) -> &ActiveQueryGuard<'me> { + self.guard.get_or_insert_with(|| { + zalsa_local.push_query(self.database_key_index, 0, cycle_strategy) + }) } - pub(super) fn into_inner(self, zalsa_local: &'me ZalsaLocal) -> ActiveQueryGuard<'me> { + #[inline] + pub(super) fn into_inner( + self, + zalsa_local: &'me ZalsaLocal, + cycle_strategy: CycleRecoveryStrategy, + ) -> ActiveQueryGuard<'me> { self.guard - .unwrap_or_else(|| zalsa_local.push_query(self.database_key_index, 0)) + .unwrap_or_else(|| zalsa_local.push_query(self.database_key_index, 0, cycle_strategy)) } } diff --git a/src/function/maybe_changed_after.rs b/src/function/maybe_changed_after.rs index 436de6e27..7749e2d40 100644 --- a/src/function/maybe_changed_after.rs +++ b/src/function/maybe_changed_after.rs @@ -111,14 +111,14 @@ where ) { ClaimResult::Retry => return None, ClaimResult::Cycle => match C::CYCLE_STRATEGY { - CycleRecoveryStrategy::Panic => db.zalsa_local().with_query_stack(|stack| { - panic!( - "dependency graph cycle when validating {database_key_index:#?}, \ - set cycle_fn/cycle_initial to fixpoint iterate.\n\ - Query stack:\n{:#?}", - stack, - ); - }), + CycleRecoveryStrategy::Panic => db + .zalsa_local() + .cycle_panic(database_key_index, "validating"), + CycleRecoveryStrategy::FallbackImmediate => { + db.zalsa_local() + .assert_top_non_panic_cycle(database_key_index); + return Some(VerifyResult::unchanged()); + } CycleRecoveryStrategy::Fixpoint => { return Some(VerifyResult::Unchanged( InputAccumulatedValues::Empty, @@ -159,7 +159,7 @@ where if old_memo.value.is_some() { let memo = self.execute( db, - active_query.into_inner(db.zalsa_local()), + active_query.into_inner(db.zalsa_local(), C::CYCLE_STRATEGY), Some(old_memo), ); let changed_at = memo.revisions.changed_at; @@ -376,7 +376,7 @@ where return VerifyResult::Changed; } - let _guard = active_query.guard(db.zalsa_local()); + let _guard = active_query.guard(db.zalsa_local(), C::CYCLE_STRATEGY); let mut cycle_heads = CycleHeads::default(); 'cycle: loop { diff --git a/src/function/memo.rs b/src/function/memo.rs index eaa315cb2..9a14ba94d 100644 --- a/src/function/memo.rs +++ b/src/function/memo.rs @@ -5,7 +5,7 @@ use std::fmt::{Debug, Formatter}; use std::ptr::NonNull; use std::sync::atomic::Ordering; -use crate::cycle::{CycleHeads, CycleRecoveryStrategy, EMPTY_CYCLE_HEADS}; +use crate::cycle::{CycleHeads, EMPTY_CYCLE_HEADS}; use crate::function::{Configuration, IngredientImpl}; use crate::key::DatabaseKeyIndex; use crate::revision::AtomicRevision; @@ -106,17 +106,6 @@ impl IngredientImpl { table.map_memo(memo_ingredient_index, map) } - - pub(super) fn initial_value<'db>( - &'db self, - db: &'db C::DbView, - key: Id, - ) -> Option> { - match C::CYCLE_STRATEGY { - CycleRecoveryStrategy::Fixpoint => Some(C::cycle_initial(db, C::id_to_input(db, key))), - CycleRecoveryStrategy::Panic => None, - } - } } #[derive(Debug)] diff --git a/src/zalsa_local.rs b/src/zalsa_local.rs index 70d48e506..15da4ef11 100644 --- a/src/zalsa_local.rs +++ b/src/zalsa_local.rs @@ -7,7 +7,7 @@ use tracing::debug; use crate::accumulator::accumulated_map::{AccumulatedMap, AtomicInputAccumulatedValues}; use crate::active_query::QueryStack; -use crate::cycle::CycleHeads; +use crate::cycle::{CycleHeads, CycleRecoveryStrategy}; use crate::durability::Durability; use crate::key::DatabaseKeyIndex; use crate::runtime::Stamp; @@ -89,9 +89,10 @@ impl ZalsaLocal { &self, database_key_index: DatabaseKeyIndex, iteration_count: u32, + cycle_strategy: CycleRecoveryStrategy, ) -> ActiveQueryGuard<'_> { let mut query_stack = self.query_stack.borrow_mut(); - query_stack.push_new_query(database_key_index, iteration_count); + query_stack.push_new_query(database_key_index, iteration_count, cycle_strategy); ActiveQueryGuard { local_state: self, database_key_index, @@ -157,6 +158,37 @@ impl ZalsaLocal { }) } + #[track_caller] + pub(crate) fn assert_top_non_panic_cycle(&self, database_key_index: DatabaseKeyIndex) { + self.with_query_stack(|stack| { + let top_differs = stack + .iter() + .rev() + .find(|query| query.cycle_strategy() != CycleRecoveryStrategy::Panic) + .is_some_and(|q| q.database_key_index != database_key_index); + if top_differs { + panic!( + "fallback immediate cycle containing multiple fallback \ + immediate queries when validating {database_key_index:#?}, \ + query stack:\n{:#?}", + stack, + ); + } + }) + } + + #[track_caller] + pub(crate) fn cycle_panic(&self, database_key_index: DatabaseKeyIndex, operation: &str) -> ! { + self.with_query_stack(|stack| { + panic!( + "dependency graph cycle when {operation} {database_key_index:#?}, \ + set cycle_fn/cycle_initial to fixpoint iterate.\n\ + query stack:\n{:#?}", + stack, + ); + }) + } + /// Register that currently active query reads the given input #[inline(always)] pub(crate) fn report_tracked_read( diff --git a/tests/cycle_fallback_immediate.rs b/tests/cycle_fallback_immediate.rs new file mode 100644 index 000000000..c85d99ce3 --- /dev/null +++ b/tests/cycle_fallback_immediate.rs @@ -0,0 +1,23 @@ +//! It is possible to omit the `cycle_fn`, only specifying `cycle_result` in which case +//! an immediate fallback value is used as the cycle handling opposed to doing a fixpoint resolution. +#[salsa::tracked] +fn zero(_db: &dyn salsa::Database) -> u32 { + 0 +} + +#[salsa::tracked(cycle_result=cycle_result)] +fn one_o_one(db: &dyn salsa::Database) -> u32 { + let val = one_o_one(db); + val + 1 +} + +fn cycle_result(_db: &dyn salsa::Database) -> u32 { + 100 +} + +#[test_log::test] +fn the_test() { + let db = salsa::DatabaseImpl::default(); + + assert_eq!(one_o_one(&db), 101); +} diff --git a/tests/cycle_fallback_left_right.rs b/tests/cycle_fallback_left_right.rs new file mode 100644 index 000000000..a3bdfd380 --- /dev/null +++ b/tests/cycle_fallback_left_right.rs @@ -0,0 +1,30 @@ +/// A test showing the dependence on cycle entry points for `cycle_result` handling. +#[salsa::tracked(cycle_result=cycle_result)] +fn left(db: &dyn salsa::Database) -> u32 { + 10 * right(db) +} + +#[salsa::tracked(cycle_result=cycle_result)] +fn right(db: &dyn salsa::Database) -> u32 { + 1 + left(db) +} + +fn cycle_result(_db: &dyn salsa::Database) -> u32 { + 0 +} + +#[test_log::test] +#[should_panic = "fallback immediate cycle"] +fn left_entry() { + let db = salsa::DatabaseImpl::default(); + + assert_eq!(left(&db), 10); +} + +#[test_log::test] +#[should_panic = "fallback immediate cycle"] +fn right_entry() { + let db = salsa::DatabaseImpl::default(); + + assert_eq!(right(&db), 1); +} diff --git a/tests/parallel/cycle_a_t1_b_t2_fallback.rs b/tests/parallel/cycle_a_t1_b_t2_fallback.rs new file mode 100644 index 000000000..e350d39c3 --- /dev/null +++ b/tests/parallel/cycle_a_t1_b_t2_fallback.rs @@ -0,0 +1,75 @@ +//! Test a specific cycle scenario: +//! +//! ```text +//! Thread T1 Thread T2 +//! --------- --------- +//! | | +//! v | +//! query_a() | +//! ^ | v +//! | +------------> query_b() +//! | | +//! +--------------------+ +//! ``` + +use crate::setup::{Knobs, KnobsDatabase}; + +const FALLBACK_A: u32 = 0b01; +const FALLBACK_B: u32 = 0b10; +const OFFSET_A: u32 = 0b0100; +const OFFSET_B: u32 = 0b1000; + +// Signal 1: T1 has entered `query_a` +// Signal 2: T2 has entered `query_b` + +#[salsa::tracked(cycle_result=cycle_result_a)] +fn query_a(db: &dyn KnobsDatabase) -> u32 { + db.signal(1); + + // Wait for Thread T2 to enter `query_b` before we continue. + db.wait_for(2); + + query_b(db) | OFFSET_A +} + +#[salsa::tracked(cycle_result=cycle_result_b)] +fn query_b(db: &dyn KnobsDatabase) -> u32 { + // Wait for Thread T1 to enter `query_a` before we continue. + db.wait_for(1); + + db.signal(2); + + query_a(db) | OFFSET_B +} + +fn cycle_result_a(_db: &dyn KnobsDatabase) -> u32 { + FALLBACK_A +} + +fn cycle_result_b(_db: &dyn KnobsDatabase) -> u32 { + FALLBACK_B +} + +#[test_log::test] +#[should_panic = "fallback immediate cycle"] +fn the_test() { + std::thread::scope(|scope| { + let db_t1 = Knobs::default(); + let db_t2 = db_t1.clone(); + + let t1 = scope.spawn(move || query_a(&db_t1)); + let t2 = scope.spawn(move || query_b(&db_t2)); + + let (r_t1, r_t2) = (t1.join(), t2.join()); + + assert_eq!( + (r_t1?, r_t2?), + ( + FALLBACK_A | OFFSET_A | OFFSET_B, + FALLBACK_B | OFFSET_A | OFFSET_B, + ) + ); + Ok(()) + }) + .unwrap_or_else(|e| std::panic::resume_unwind(e)); +} diff --git a/tests/parallel/main.rs b/tests/parallel/main.rs index 613e43e1d..7507a71cf 100644 --- a/tests/parallel/main.rs +++ b/tests/parallel/main.rs @@ -1,6 +1,7 @@ mod setup; mod cycle_a_t1_b_t2; +mod cycle_a_t1_b_t2_fallback; mod cycle_ab_peeping_c; mod cycle_nested_three_threads; mod cycle_panic;