Skip to content

Commit 6fa519d

Browse files
stroxleroopscompiled
authored andcommitted
Make non-convergence errors respect the write lock
Summary: When iterative SCC solving exceeds MAX_ITERATIONS, non-convergence diagnostics were emitted before commit arbitration. Since multiple threads can independently solve the same SCC, each contender could emit the same diagnostics to the shared ErrorCollector, causing nondeterministic duplicate errors. This change makes non-convergence diagnostics follow the same winner selection as answers: - Thread write-unlock/commit plumbing now returns whether a write actually won (did_write / did_commit). - iterative_resolve_scc snapshots non-convergent members, commits final answers, and emits non-convergence diagnostics only if this thread won commit. - Cross-module write-unlock now also returns did_write for consistent winner tracking. This preserves first-write-wins behavior, eliminates duplicate loser- thread diagnostics, and keeps diagnostics authoritative to the committed SCC result. Reviewed By: rchen152 Differential Revision: D95402450 fbshipit-source-id: e3caf70e6bfb7d75a29aa08c02b5b70095cff649
1 parent b5a9746 commit 6fa519d

3 files changed

Lines changed: 65 additions & 47 deletions

File tree

pyrefly/lib/alt/answers.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -570,7 +570,8 @@ pub trait LookupAnswer: Sized {
570570
_calc_id: CalcId,
571571
_answer: Arc<dyn Any + Send + Sync>,
572572
_errors: Option<Arc<ErrorCollector>>,
573-
) {
573+
) -> bool {
574+
false
574575
}
575576

576577
/// Release a write lock on a cross-module Calculation cell without

pyrefly/lib/alt/answers_solver.rs

Lines changed: 60 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2694,14 +2694,14 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
26942694
calc_id: CalcId,
26952695
answer: Arc<dyn Any + Send + Sync>,
26962696
errors: Option<Arc<ErrorCollector>>,
2697-
) {
2697+
) -> bool {
26982698
let CalcId(ref bindings, ref any_idx) = calc_id;
26992699
if bindings.module().name() == self.bindings().module().name()
27002700
&& bindings.module().path() == self.bindings().module().path()
27012701
{
27022702
dispatch_anyidx!(any_idx, self, write_unlock_same_module, answer, errors)
27032703
} else {
2704-
self.answers.write_unlock_in_module(calc_id, answer, errors);
2704+
self.answers.write_unlock_in_module(calc_id, answer, errors)
27052705
}
27062706
}
27072707

@@ -2711,7 +2711,8 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
27112711
idx: Idx<K>,
27122712
answer: Arc<dyn Any + Send + Sync>,
27132713
errors: Option<Arc<ErrorCollector>>,
2714-
) where
2714+
) -> bool
2715+
where
27152716
AnswerTable: TableKeyed<K, Value = AnswerEntry<K>>,
27162717
BindingTable: TableKeyed<K, Value = BindingEntry<K>>,
27172718
{
@@ -2729,6 +2730,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
27292730
);
27302731
self.base_errors.extend(errors);
27312732
}
2733+
did_write
27322734
}
27332735

27342736
/// Release a write lock without writing a value (panic cleanup).
@@ -2834,7 +2836,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
28342836
///
28352837
/// Called after the fixpoint iteration converges (or max iterations are
28362838
/// reached).
2837-
fn commit_final_answers(&self, scc: Scc) {
2839+
fn commit_final_answers(&self, scc: Scc) -> bool {
28382840
let iter_state = scc
28392841
.iterative
28402842
.expect("commit_final_answers: SCC has no iteration state");
@@ -2876,12 +2878,14 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
28762878
for calc_id in &guard.locked {
28772879
locked_members.push(calc_id.dupe());
28782880
}
2881+
let mut did_write_any = false;
28792882
for (calc_id, answer, errors) in members {
28802883
if locked_members.contains(&calc_id) {
2881-
self.write_unlock_single(calc_id, answer, errors);
2884+
did_write_any |= self.write_unlock_single(calc_id, answer, errors);
28822885
}
28832886
}
28842887
guard.disarm();
2888+
did_write_any
28852889
}
28862890

28872891
/// Drive a single iteration member by calling `get_idx` for its typed index.
@@ -3094,48 +3098,60 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
30943098
// Report per-member errors for non-converging SCC members.
30953099
// Each member uses its own module's bindings and error collector
30963100
// because SCCs can span modules.
3097-
if exceeded_max_iterations {
3098-
let iter_state = scc
3099-
.iterative
3100-
.as_ref()
3101-
.expect("iterative_resolve_scc: SCC lost iteration state before error reporting");
3102-
for (calc_id, node_state) in &iter_state.node_states {
3103-
if let IterationNodeState::Done { ref answer, .. } = *node_state {
3104-
let previous = iter_state.previous_answers.get(calc_id);
3105-
let member_bindings = &calc_id.0;
3106-
if self.is_same_module(calc_id) {
3107-
dispatch_anyidx!(
3108-
&calc_id.1,
3109-
self,
3110-
check_and_report_non_convergent_member,
3111-
answer,
3112-
previous,
3113-
member_bindings,
3114-
self.base_errors
3115-
);
3116-
} else {
3117-
// Cross-module member: create a temporary collector
3118-
// with the member's module info. Each Error stores its
3119-
// own module, so extending base_errors preserves the
3120-
// correct file attribution.
3121-
let cross_errors =
3122-
ErrorCollector::new(calc_id.0.module().dupe(), ErrorStyle::Delayed);
3123-
dispatch_anyidx!(
3124-
&calc_id.1,
3125-
self,
3126-
check_and_report_non_convergent_member,
3127-
answer,
3128-
previous,
3129-
member_bindings,
3130-
&cross_errors
3131-
);
3132-
self.base_errors.extend(cross_errors);
3133-
}
3101+
let non_convergent_members: Vec<(
3102+
CalcId,
3103+
Arc<dyn Any + Send + Sync>,
3104+
Option<Arc<dyn Any + Send + Sync>>,
3105+
)> = if exceeded_max_iterations {
3106+
let iter_state = scc.iterative.as_ref().expect(
3107+
"iterative_resolve_scc: SCC lost iteration state before non-convergence extraction",
3108+
);
3109+
iter_state
3110+
.node_states
3111+
.iter()
3112+
.filter_map(|(calc_id, node_state)| match node_state {
3113+
IterationNodeState::Done { answer, .. } => Some((
3114+
calc_id.dupe(),
3115+
answer.dupe(),
3116+
iter_state.previous_answers.get(calc_id).cloned(),
3117+
)),
3118+
IterationNodeState::Fresh | IterationNodeState::InProgress { .. } => None,
3119+
})
3120+
.collect()
3121+
} else {
3122+
Vec::new()
3123+
};
3124+
3125+
let did_commit = self.commit_final_answers(scc);
3126+
if did_commit {
3127+
for (calc_id, answer, previous) in &non_convergent_members {
3128+
let member_bindings = &calc_id.0;
3129+
if self.is_same_module(calc_id) {
3130+
dispatch_anyidx!(
3131+
&calc_id.1,
3132+
self,
3133+
check_and_report_non_convergent_member,
3134+
answer,
3135+
previous.as_ref(),
3136+
member_bindings,
3137+
self.base_errors
3138+
);
3139+
} else {
3140+
let cross_errors =
3141+
ErrorCollector::new(calc_id.0.module().dupe(), ErrorStyle::Delayed);
3142+
dispatch_anyidx!(
3143+
&calc_id.1,
3144+
self,
3145+
check_and_report_non_convergent_member,
3146+
answer,
3147+
previous.as_ref(),
3148+
member_bindings,
3149+
&cross_errors
3150+
);
3151+
self.base_errors.extend(cross_errors);
31343152
}
31353153
}
31363154
}
3137-
3138-
self.commit_final_answers(scc);
31393155
}
31403156

31413157
/// Finalize a recursive answer. This takes the raw value produced by `K::solve` and calls

pyrefly/lib/state/state.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2498,10 +2498,10 @@ impl<'a> LookupAnswer for TransactionHandle<'a> {
24982498
calc_id: CalcId,
24992499
answer: Arc<dyn Any + Send + Sync>,
25002500
errors: Option<Arc<ErrorCollector>>,
2501-
) {
2501+
) -> bool {
25022502
let CalcId(_, ref any_idx) = calc_id;
25032503
match self.lookup_target_answers(&calc_id) {
2504-
TargetAnswers::ModuleNotFound | TargetAnswers::Evicted => {}
2504+
TargetAnswers::ModuleNotFound | TargetAnswers::Evicted => false,
25052505
TargetAnswers::Available { answers, load, .. } => {
25062506
let did_write = answers.write_unlock_preliminary(any_idx, answer);
25072507
if did_write && let (Some(errors), Some(target_load)) = (errors, load) {
@@ -2510,6 +2510,7 @@ impl<'a> LookupAnswer for TransactionHandle<'a> {
25102510
);
25112511
target_load.errors.extend(errors);
25122512
}
2513+
did_write
25132514
}
25142515
}
25152516
}

0 commit comments

Comments
 (0)