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 80 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
3d00ae3
add example test for fixpoint iteration
carljm Aug 28, 2024
31979b9
add a multi-symbol test
carljm Aug 28, 2024
35e236e
simplify test case
carljm Sep 21, 2024
90ea6e9
WIP: remove existing cycle handling tests for now
carljm Oct 8, 2024
f218e58
WIP: remove all existing cycle handling, add fixpoint options
carljm Oct 9, 2024
6ce61c6
WIP: added provisional value and cycle fields
carljm Oct 12, 2024
29d110e
rename to CycleRecoveryStrategy::Fixpoint
carljm Oct 17, 2024
adb6c77
WIP: rip out ProvisionalValue
carljm Oct 23, 2024
7f82b6d
WIP: working single-iteration with provisional memo
carljm Oct 23, 2024
a7be3d9
WIP: add count arg to cycle_recovery_fn
carljm Oct 23, 2024
6c6dd55
WIP: move insert-initial out into fetch_cold
carljm Oct 23, 2024
8974361
WIP: cycle-head iteration
carljm Oct 23, 2024
797655e
WIP: move loop into execute
carljm Oct 23, 2024
27742a2
WIP: delay storing memo
carljm Oct 23, 2024
4f4df72
WIP: remove ourself from cycle heads when done iterating
carljm Oct 23, 2024
315944e
WIP: working convergence and fallback
carljm Oct 23, 2024
28242a4
WIP: clippy and cleanup
carljm Oct 23, 2024
0be825a
WIP: improve comments and add a type annotation
carljm Oct 24, 2024
c3c84c4
WIP: don't allow cycle_fn with no_eq
carljm Oct 24, 2024
72dff5f
WIP: add tracing for cycle iteration
carljm Oct 24, 2024
6b44c92
WIP: fail fast if we get an evicted provisional value
carljm Oct 24, 2024
a029ef4
WIP: use FxHashSet::from_iter
carljm Oct 24, 2024
492ae1b
add tests, fix multiple-revision, lazy provisional value
carljm Oct 30, 2024
0f7d940
review feedback, more tracing
carljm Oct 31, 2024
5ef7a5f
fix multi-revision bug
carljm Oct 31, 2024
3093460
better fix for multi-revision bug
carljm Oct 31, 2024
7d9ec1c
test fixes
carljm Oct 31, 2024
aa4a731
pass inputs to cycle recovery functions
carljm Nov 1, 2024
67376f1
fixed cycle-unchanged test
carljm Nov 15, 2024
286b5fb
add TODO comments for some outstanding questions
carljm Nov 15, 2024
b2d4d92
add a test for the "AB peeping C" scenario
Nov 15, 2024
670f88b
another parallel test scenario
Nov 15, 2024
00acc56
WIP: removed cycle_ignore; nested cycles broken
carljm Dec 14, 2024
5202579
fixed all single-thread cycles; multi-thread still not working
carljm Dec 18, 2024
1dfa978
Merge branch 'master' into fixpoint
carljm Jan 14, 2025
ea62531
Merge branch 'master' into fixpoint
carljm Jan 14, 2025
3bf7930
panic if fallback fails to converge
carljm Jan 15, 2025
cec63ba
Merge branch 'master' into fixpoint
carljm Jan 16, 2025
33ac6ac
all cycle tests passing
carljm Jan 23, 2025
e7e511b
remove debugging cruft
carljm Jan 23, 2025
3f84bae
Merge branch 'master' into fixpoint
carljm Jan 23, 2025
68f932a
fix expect message
carljm Jan 23, 2025
2aa0ff0
revert change to compile-fail test expectation
carljm Jan 23, 2025
3b78901
a couple optimizations
carljm Jan 23, 2025
d6a73a9
construct fewer hash sets
carljm Jan 23, 2025
9cc059b
Merge branch 'master' into fixpoint
carljm Jan 24, 2025
7d9ced3
three-thread cycle test
carljm Jan 24, 2025
d4217ca
Merge branch 'master' into fixpoint
carljm Jan 25, 2025
16d4a49
Merge branch 'master' into fixpoint
carljm Jan 28, 2025
3ae4c8a
Merge branch 'master' into fixpoint
carljm Jan 28, 2025
e659993
Merge branch 'master' into fixpoint
carljm Jan 31, 2025
8df14b6
fix multi-threaded cycle tests on Linux
carljm Feb 4, 2025
70ecf57
provisional memo can race with cycle completing in other thread
carljm Feb 4, 2025
68717af
assert provisional memo is provisional
carljm Feb 4, 2025
3d4d47c
Merge branch 'master' into fixpoint
carljm Feb 7, 2025
edb0910
test calling query from cycle fns
carljm Feb 7, 2025
c797218
add dataflow benchmark for fixpoint iteration
carljm Feb 7, 2025
76d048e
reduce size of ClaimResult
carljm Feb 7, 2025
b5808f8
Merge branch 'master' into fixpoint
carljm Feb 7, 2025
222f9e5
add tests for calling back into a cycle from initial/recovery fns
carljm Feb 8, 2025
c4073f6
restructure threaded cycle tests for easier debugging of failures
carljm Feb 8, 2025
2c00840
is_verified_final/wait_for are true for non-function ingredients
carljm Feb 8, 2025
bc59685
doc comment updates
carljm Feb 8, 2025
9508f32
use AtomicBool instead of AtomicCell<bool>
carljm Feb 8, 2025
20bc3b2
Merge branch 'master' into fixpoint
carljm Feb 10, 2025
8398398
fix some bugs in deep_verify_memo
carljm Feb 11, 2025
0543ba2
fix clippy
carljm Feb 11, 2025
20c5424
add comments on allow_provisional and validate_provisional
carljm Feb 11, 2025
2685331
improve comments
carljm Feb 11, 2025
5794e3e
Merge branch 'master' into fixpoint
carljm Feb 11, 2025
d60ceb0
Merge branch 'master' into fixpoint
carljm Feb 12, 2025
60db5ad
minor improvements to refresh_memo
carljm Feb 14, 2025
ba2bbc1
fix nested cycles bug
carljm Feb 14, 2025
780f8fe
use boxed hashset for cycle heads
carljm Feb 14, 2025
950216b
avoid allocating hashset with nothing to put in it
carljm Feb 14, 2025
3aacf16
ensure an empty cycle heads is always None
carljm Feb 14, 2025
88ec26e
actually ensure we always use None
carljm Feb 14, 2025
7678d13
avoid double Option
carljm Feb 14, 2025
6ffdb43
extract Memo::provisional_retry from inlined refresh_memo
carljm Feb 14, 2025
057d6c9
avoid jumps in refresh_memo
carljm Feb 14, 2025
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
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ crossbeam-channel = "0.5.14"
name = "compare"
harness = false


[[bench]]
name = "incremental"
harness = false
Expand All @@ -56,5 +55,9 @@ harness = false
name = "accumulator"
harness = false

[[bench]]
name = "dataflow"
harness = false

[workspace]
members = ["components/salsa-macro-rules", "components/salsa-macros"]
170 changes: 170 additions & 0 deletions benches/dataflow.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
//! Benchmark for fixpoint iteration cycle resolution.
//!
//! This benchmark simulates a (very simplified) version of a real dataflow analysis using fixpoint
//! iteration.
use codspeed_criterion_compat::{criterion_group, criterion_main, BatchSize, Criterion};
use salsa::{CycleRecoveryAction, Database as Db, Setter};
use std::collections::BTreeSet;
use std::iter::IntoIterator;

/// A Use of a symbol.
#[salsa::input]
struct Use {
reaching_definitions: Vec<Definition>,
}

/// A Definition of a symbol, either of the form `base + increment` or `0 + increment`.
#[salsa::input]
struct Definition {
base: Option<Use>,
increment: usize,
}

#[derive(Eq, PartialEq, Clone, Debug, salsa::Update)]
enum Type {
Bottom,
Values(Box<[usize]>),
Top,
}

impl Type {
fn join(tys: impl IntoIterator<Item = Type>) -> Type {
let mut result = Type::Bottom;
for ty in tys.into_iter() {
result = match (result, ty) {
(result, Type::Bottom) => result,
(_, Type::Top) => Type::Top,
(Type::Top, _) => Type::Top,
(Type::Bottom, ty) => ty,
(Type::Values(a_ints), Type::Values(b_ints)) => {
let mut set = BTreeSet::new();
set.extend(a_ints);
set.extend(b_ints);
Type::Values(set.into_iter().collect())
}
}
}
result
}
}

#[salsa::tracked(cycle_fn=use_cycle_recover, cycle_initial=use_cycle_initial)]
fn infer_use<'db>(db: &'db dyn Db, u: Use) -> Type {
let defs = u.reaching_definitions(db);
match defs[..] {
[] => Type::Bottom,
[def] => infer_definition(db, def),
_ => Type::join(defs.iter().map(|&def| infer_definition(db, def))),
}
}

#[salsa::tracked(cycle_fn=def_cycle_recover, cycle_initial=def_cycle_initial)]
fn infer_definition<'db>(db: &'db dyn Db, def: Definition) -> Type {
let increment_ty = Type::Values(Box::from([def.increment(db)]));
if let Some(base) = def.base(db) {
let base_ty = infer_use(db, base);
add(&base_ty, &increment_ty)
} else {
increment_ty
}
}

fn def_cycle_initial(_db: &dyn Db, _def: Definition) -> Type {
Type::Bottom
}

fn def_cycle_recover(
_db: &dyn Db,
value: &Type,
count: u32,
_def: Definition,
) -> CycleRecoveryAction<Type> {
cycle_recover(value, count)
}

fn use_cycle_initial(_db: &dyn Db, _use: Use) -> Type {
Type::Bottom
}

fn use_cycle_recover(
_db: &dyn Db,
value: &Type,
count: u32,
_use: Use,
) -> CycleRecoveryAction<Type> {
cycle_recover(value, count)
}

fn cycle_recover(value: &Type, count: u32) -> CycleRecoveryAction<Type> {
match value {
Type::Bottom => CycleRecoveryAction::Iterate,
Type::Values(_) => {
if count > 4 {
CycleRecoveryAction::Fallback(Type::Top)
} else {
CycleRecoveryAction::Iterate
}
}
Type::Top => CycleRecoveryAction::Iterate,
}
}

fn add(a: &Type, b: &Type) -> Type {
match (a, b) {
(Type::Bottom, _) | (_, Type::Bottom) => Type::Bottom,
(Type::Top, _) | (_, Type::Top) => Type::Top,
(Type::Values(a_ints), Type::Values(b_ints)) => {
let mut set = BTreeSet::new();
set.extend(
a_ints
.into_iter()
.flat_map(|a| b_ints.into_iter().map(move |b| a + b)),
);
Type::Values(set.into_iter().collect())
}
}
}

fn dataflow(criterion: &mut Criterion) {
criterion.bench_function("converge_diverge", |b| {
b.iter_batched_ref(
|| {
let mut db = salsa::DatabaseImpl::new();

let defx0 = Definition::new(&db, None, 0);
let defy0 = Definition::new(&db, None, 0);
let defx1 = Definition::new(&db, None, 0);
let defy1 = Definition::new(&db, None, 0);
let use_x = Use::new(&db, vec![defx0, defx1]);
let use_y = Use::new(&db, vec![defy0, defy1]);
defx1.set_base(&mut db).to(Some(use_y));
defy1.set_base(&mut db).to(Some(use_x));

// prewarm cache
let _ = infer_use(&db, use_x);
let _ = infer_use(&db, use_y);

(db, defx1, use_x, use_y)
},
|(db, defx1, use_x, use_y)| {
// Set the increment on x to 0.
defx1.set_increment(db).to(0);

// Both symbols converge on 0.
assert_eq!(infer_use(db, *use_x), Type::Values(Box::from([0])));
assert_eq!(infer_use(db, *use_y), Type::Values(Box::from([0])));

// Set the increment on x to 1.
defx1.set_increment(db).to(1);

// Now the loop diverges and we fall back to Top.
assert_eq!(infer_use(db, *use_x), Type::Top);
assert_eq!(infer_use(db, *use_y), Type::Top);
},
BatchSize::LargeInput,
);
});
}

criterion_group!(benches, dataflow);
criterion_main!(benches);
18 changes: 13 additions & 5 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 @@ -168,15 +171,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 @@ -187,12 +190,17 @@ macro_rules! setup_tracked_fn {
$inner($db, $($input_id),*)
}

fn cycle_initial<$db_lt>(db: &$db_lt dyn $Db, ($($input_id),*): ($($input_ty),*)) -> Self::Output<$db_lt> {
$($cycle_recovery_initial)*(db, $($input_id),*)
}

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

fn id_to_input<$db_lt>(db: &$db_lt Self::DbView, key: salsa::Id) -> Self::Input<$db_lt> {
Expand Down
21 changes: 14 additions & 7 deletions components/salsa-macro-rules/src/unexpected_cycle_recovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,18 @@
// a macro because it can take a variadic number of arguments.
#[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, $($other_inputs:ident),*) => {{
std::mem::drop($db);
std::mem::drop(($($other_inputs),*));
panic!("cannot recover from cycle")
}};
}

#[macro_export]
macro_rules! unexpected_cycle_initial {
($db:ident, $($other_inputs:ident),*) => {{
std::mem::drop($db);
std::mem::drop(($($other_inputs),*));
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 @@ -40,7 +40,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;
const ID: 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 @@ -52,7 +52,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 @@ -53,7 +53,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 @@ -50,10 +50,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 @@ -92,7 +97,8 @@ impl<A: AllowedOptions> Default for Options<A> {
no_lifetime: 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 @@ -114,7 +120,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;
const ID: bool;
Expand Down Expand Up @@ -237,20 +244,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
Loading