Skip to content

Commit 0690fcc

Browse files
committed
Unwind with specific type when encountering an unexpected cycle
1 parent 13a2bd7 commit 0690fcc

File tree

8 files changed

+69
-50
lines changed

8 files changed

+69
-50
lines changed

src/cancelled.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ impl Cancelled {
2323
pub(crate) fn throw(self) -> ! {
2424
// We use resume and not panic here to avoid running the panic
2525
// hook (that is, to avoid collecting and printing backtrace).
26-
std::panic::resume_unwind(Box::new(self));
26+
panic::resume_unwind(Box::new(self));
2727
}
2828

2929
/// Runs `f`, and catches any salsa cancellation.

src/cycle.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@
4949
//! cycle head may then iterate, which may result in a new set of iterations on the inner cycle,
5050
//! for each iteration of the outer cycle.
5151
52+
use core::fmt;
53+
use std::panic;
54+
5255
use thin_vec::{thin_vec, ThinVec};
5356

5457
use crate::key::DatabaseKeyIndex;
@@ -58,6 +61,52 @@ use crate::key::DatabaseKeyIndex;
5861
/// Should only be relevant in case of a badly configured cycle recovery.
5962
pub const MAX_ITERATIONS: u32 = 200;
6063

64+
pub struct UnexpectedCycle(Option<crate::Backtrace>);
65+
66+
impl fmt::Debug for UnexpectedCycle {
67+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68+
f.write_str("cycle detected but no cycle handler found")?;
69+
if let Some(backtrace) = &self.0 {
70+
f.write_str(": ")?;
71+
backtrace.fmt(f)?;
72+
}
73+
Ok(())
74+
}
75+
}
76+
77+
impl fmt::Display for UnexpectedCycle {
78+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79+
f.write_str("cycle detected but no cycle handler found")?;
80+
if let Some(backtrace) = &self.0 {
81+
f.write_str("\n")?;
82+
backtrace.fmt(f)?;
83+
}
84+
Ok(())
85+
}
86+
}
87+
88+
impl UnexpectedCycle {
89+
pub(crate) fn throw() -> ! {
90+
// We use resume and not panic here to avoid running the panic
91+
// hook (that is, to avoid collecting and printing backtrace).
92+
panic::resume_unwind(Box::new(Self(crate::Backtrace::capture())));
93+
}
94+
95+
/// Runs `f`, and catches any salsa cancellation.
96+
pub fn catch<F, T>(f: F) -> Result<T, UnexpectedCycle>
97+
where
98+
F: FnOnce() -> T + panic::UnwindSafe,
99+
{
100+
match panic::catch_unwind(f) {
101+
Ok(t) => Ok(t),
102+
Err(payload) => match payload.downcast() {
103+
Ok(cancelled) => Err(*cancelled),
104+
Err(payload) => panic::resume_unwind(payload),
105+
},
106+
}
107+
}
108+
}
109+
61110
/// Return value from a cycle recovery function.
62111
#[derive(Debug)]
63112
pub enum CycleRecoveryAction<T> {

src/function/fetch.rs

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::cycle::{CycleHeads, CycleRecoveryStrategy};
1+
use crate::cycle::{CycleHeads, CycleRecoveryStrategy, UnexpectedCycle};
22
use crate::function::memo::Memo;
33
use crate::function::sync::ClaimResult;
44
use crate::function::{Configuration, IngredientImpl, VerifyResult};
@@ -125,22 +125,14 @@ where
125125
}
126126
// no provisional value; create/insert/return initial provisional value
127127
return match C::CYCLE_STRATEGY {
128-
CycleRecoveryStrategy::Panic => db.zalsa_local().with_query_stack(|stack| {
129-
panic!(
130-
"dependency graph cycle when querying {database_key_index:#?}, \
131-
set cycle_fn/cycle_initial to fixpoint iterate.\n\
132-
Query stack:\n{stack:#?}",
133-
);
134-
}),
128+
CycleRecoveryStrategy::Panic => UnexpectedCycle::throw(),
135129
CycleRecoveryStrategy::Fixpoint => {
136130
tracing::debug!(
137131
"hit cycle at {database_key_index:#?}, \
138132
inserting and returning fixpoint initial value"
139133
);
140134
let revisions = QueryRevisions::fixpoint_initial(database_key_index);
141-
let initial_value = self
142-
.initial_value(db, id)
143-
.expect("`CycleRecoveryStrategy::Fixpoint` should have initial_value");
135+
let initial_value = C::cycle_initial(db, C::id_to_input(db, id));
144136
Some(self.insert_memo(
145137
zalsa,
146138
id,
@@ -153,9 +145,7 @@ where
153145
"hit a `FallbackImmediate` cycle at {database_key_index:#?}"
154146
);
155147
let active_query = db.zalsa_local().push_query(database_key_index, 0);
156-
let fallback_value = self.initial_value(db, id).expect(
157-
"`CycleRecoveryStrategy::FallbackImmediate` should have initial_value",
158-
);
148+
let fallback_value = C::cycle_initial(db, C::id_to_input(db, id));
159149
let mut revisions = active_query.pop();
160150
revisions.cycle_heads = CycleHeads::initial(database_key_index);
161151
// We need this for `cycle_heads()` to work. We will unset this in the outer `execute()`.

src/function/maybe_changed_after.rs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::accumulator::accumulated_map::InputAccumulatedValues;
2-
use crate::cycle::{CycleHeadKind, CycleHeads, CycleRecoveryStrategy};
2+
use crate::cycle::{CycleHeadKind, CycleHeads, CycleRecoveryStrategy, UnexpectedCycle};
33
use crate::function::memo::Memo;
44
use crate::function::sync::ClaimResult;
55
use crate::function::{Configuration, IngredientImpl};
@@ -104,13 +104,7 @@ where
104104
let _claim_guard = match self.sync_table.try_claim(zalsa, key_index) {
105105
ClaimResult::Retry => return None,
106106
ClaimResult::Cycle => match C::CYCLE_STRATEGY {
107-
CycleRecoveryStrategy::Panic => db.zalsa_local().with_query_stack(|stack| {
108-
panic!(
109-
"dependency graph cycle when validating {database_key_index:#?}, \
110-
set cycle_fn/cycle_initial to fixpoint iterate.\n\
111-
Query stack:\n{stack:#?}",
112-
);
113-
}),
107+
CycleRecoveryStrategy::Panic => UnexpectedCycle::throw(),
114108
CycleRecoveryStrategy::FallbackImmediate => {
115109
return Some(VerifyResult::unchanged());
116110
}

src/function/memo.rs

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::fmt::{Debug, Formatter};
33
use std::mem::transmute;
44
use std::ptr::NonNull;
55

6-
use crate::cycle::{CycleHeadKind, CycleHeads, CycleRecoveryStrategy, EMPTY_CYCLE_HEADS};
6+
use crate::cycle::{CycleHeadKind, CycleHeads, EMPTY_CYCLE_HEADS};
77
use crate::function::{Configuration, IngredientImpl};
88
use crate::key::DatabaseKeyIndex;
99
use crate::loom::sync::atomic::Ordering;
@@ -82,19 +82,6 @@ impl<C: Configuration> IngredientImpl<C> {
8282

8383
table.map_memo(memo_ingredient_index, map)
8484
}
85-
86-
pub(super) fn initial_value<'db>(
87-
&'db self,
88-
db: &'db C::DbView,
89-
key: Id,
90-
) -> Option<C::Output<'db>> {
91-
match C::CYCLE_STRATEGY {
92-
CycleRecoveryStrategy::Fixpoint | CycleRecoveryStrategy::FallbackImmediate => {
93-
Some(C::cycle_initial(db, C::id_to_input(db, key)))
94-
}
95-
CycleRecoveryStrategy::Panic => None,
96-
}
97-
}
9885
}
9986

10087
#[derive(Debug)]

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ pub use salsa_macros::{accumulator, db, input, interned, tracked, Supertype, Upd
4242
pub use self::accumulator::Accumulator;
4343
pub use self::active_query::Backtrace;
4444
pub use self::cancelled::Cancelled;
45-
pub use self::cycle::CycleRecoveryAction;
45+
pub use self::cycle::{CycleRecoveryAction, UnexpectedCycle};
4646
pub use self::database::{AsDynDatabase, Database};
4747
pub use self::database_impl::DatabaseImpl;
4848
pub use self::durability::Durability;

tests/cycle.rs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
mod common;
66
use common::{ExecuteValidateLoggerDatabase, LogDatabase};
77
use expect_test::expect;
8-
use salsa::{CycleRecoveryAction, Database as Db, DatabaseImpl as DbImpl, Durability, Setter};
8+
use salsa::{
9+
CycleRecoveryAction, Database as Db, DatabaseImpl as DbImpl, Durability, Setter,
10+
UnexpectedCycle,
11+
};
912
#[cfg(not(miri))]
1013
use test_log::test;
1114

@@ -222,14 +225,12 @@ fn value(num: u8) -> Input {
222225
///
223226
/// Simple self-cycle, no iteration, should panic.
224227
#[test]
225-
#[should_panic(expected = "dependency graph cycle")]
226228
fn self_panic() {
227229
let mut db = DbImpl::new();
228230
let a_in = Inputs::new(&db, vec![]);
229231
let a = Input::MinPanic(a_in);
230232
a_in.set_inputs(&mut db).to(vec![a.clone()]);
231-
232-
a.eval(&db);
233+
UnexpectedCycle::catch(|| a.eval(&db)).unwrap_err();
233234
}
234235

235236
/// a:Np(u10, a) -+
@@ -238,14 +239,13 @@ fn self_panic() {
238239
///
239240
/// Simple self-cycle with untracked read, no iteration, should panic.
240241
#[test]
241-
#[should_panic(expected = "dependency graph cycle")]
242242
fn self_untracked_panic() {
243243
let mut db = DbImpl::new();
244244
let a_in = Inputs::new(&db, vec![]);
245245
let a = Input::MinPanic(a_in);
246246
a_in.set_inputs(&mut db).to(vec![untracked(10), a.clone()]);
247247

248-
a.eval(&db);
248+
UnexpectedCycle::catch(|| a.eval(&db)).unwrap_err();
249249
}
250250

251251
/// a:Ni(a) -+
@@ -289,7 +289,6 @@ fn two_mixed_converge_initial_value() {
289289
/// Two-query cycle, one with iteration and one without.
290290
/// If we enter from the one with no iteration, we panic.
291291
#[test]
292-
#[should_panic(expected = "dependency graph cycle")]
293292
fn two_mixed_panic() {
294293
let mut db = DbImpl::new();
295294
let a_in = Inputs::new(&db, vec![]);
@@ -299,7 +298,7 @@ fn two_mixed_panic() {
299298
a_in.set_inputs(&mut db).to(vec![b]);
300299
b_in.set_inputs(&mut db).to(vec![a.clone()]);
301300

302-
a.eval(&db);
301+
UnexpectedCycle::catch(|| a.eval(&db)).unwrap_err();
303302
}
304303

305304
/// a:Ni(b) --> b:Xi(a)
@@ -370,7 +369,6 @@ fn two_indirect_iterate_converge_initial_value() {
370369
///
371370
/// Two-query cycle, enter indirectly at node without iteration, panic.
372371
#[test]
373-
#[should_panic(expected = "dependency graph cycle")]
374372
fn two_indirect_panic() {
375373
let mut db = DbImpl::new();
376374
let a_in = Inputs::new(&db, vec![]);
@@ -383,7 +381,7 @@ fn two_indirect_panic() {
383381
b_in.set_inputs(&mut db).to(vec![c]);
384382
c_in.set_inputs(&mut db).to(vec![b]);
385383

386-
a.eval(&db);
384+
UnexpectedCycle::catch(|| a.eval(&db)).unwrap_err();
387385
}
388386

389387
/// a:Np(b) -> b:Ni(v200,c) -> c:Xp(b)

tests/cycle_initial_call_back_into_cycle.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
//! Calling back into the same cycle from your cycle initial function will trigger another cycle.
22
3+
use salsa::UnexpectedCycle;
4+
35
#[salsa::tracked]
46
fn initial_value(db: &dyn salsa::Database) -> u32 {
57
query(db)
@@ -28,9 +30,8 @@ fn cycle_fn(
2830
}
2931

3032
#[test_log::test]
31-
#[should_panic(expected = "dependency graph cycle")]
3233
fn the_test() {
3334
let db = salsa::DatabaseImpl::default();
3435

35-
query(&db);
36+
UnexpectedCycle::catch(|| query(&db)).unwrap_err();
3637
}

0 commit comments

Comments
 (0)