Skip to content

Commit 4a0c7cc

Browse files
committed
fix cycle head usages tracking
1 parent 5b9007b commit 4a0c7cc

File tree

3 files changed

+166
-52
lines changed

3 files changed

+166
-52
lines changed

compiler/rustc_next_trait_solver/src/solve/search_graph.rs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,21 @@ where
7474
}
7575
}
7676

77-
fn is_initial_provisional_result(
78-
cx: Self::Cx,
79-
kind: PathKind,
80-
input: CanonicalInput<I>,
81-
result: QueryResult<I>,
82-
) -> bool {
83-
Self::initial_provisional_result(cx, kind, input) == result
77+
fn is_initial_provisional_result(result: QueryResult<I>) -> Option<PathKind> {
78+
match result {
79+
Ok(response) => {
80+
if has_no_inference_or_external_constraints(response) {
81+
if response.value.certainty == Certainty::Yes {
82+
return Some(PathKind::Coinductive);
83+
} else if response.value.certainty == Certainty::overflow(false) {
84+
return Some(PathKind::Unknown);
85+
}
86+
}
87+
88+
None
89+
}
90+
Err(NoSolution) => Some(PathKind::Inductive),
91+
}
8492
}
8593

8694
fn on_stack_overflow(cx: I, input: CanonicalInput<I>) -> QueryResult<I> {

compiler/rustc_type_ir/src/search_graph/mod.rs

Lines changed: 96 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,7 @@ pub trait Delegate: Sized {
8686
kind: PathKind,
8787
input: <Self::Cx as Cx>::Input,
8888
) -> <Self::Cx as Cx>::Result;
89-
fn is_initial_provisional_result(
90-
cx: Self::Cx,
91-
kind: PathKind,
92-
input: <Self::Cx as Cx>::Input,
93-
result: <Self::Cx as Cx>::Result,
94-
) -> bool;
89+
fn is_initial_provisional_result(result: <Self::Cx as Cx>::Result) -> Option<PathKind>;
9590
fn on_stack_overflow(cx: Self::Cx, input: <Self::Cx as Cx>::Input) -> <Self::Cx as Cx>::Result;
9691
fn on_fixpoint_overflow(
9792
cx: Self::Cx,
@@ -215,6 +210,27 @@ impl HeadUsages {
215210
let HeadUsages { inductive, unknown, coinductive, forced_ambiguity } = self;
216211
inductive == 0 && unknown == 0 && coinductive == 0 && forced_ambiguity == 0
217212
}
213+
214+
fn is_single(self, path_kind: PathKind) -> bool {
215+
match path_kind {
216+
PathKind::Inductive => matches!(
217+
self,
218+
HeadUsages { inductive: _, unknown: 0, coinductive: 0, forced_ambiguity: 0 },
219+
),
220+
PathKind::Unknown => matches!(
221+
self,
222+
HeadUsages { inductive: 0, unknown: _, coinductive: 0, forced_ambiguity: 0 },
223+
),
224+
PathKind::Coinductive => matches!(
225+
self,
226+
HeadUsages { inductive: 0, unknown: 0, coinductive: _, forced_ambiguity: 0 },
227+
),
228+
PathKind::ForcedAmbiguity => matches!(
229+
self,
230+
HeadUsages { inductive: 0, unknown: 0, coinductive: 0, forced_ambiguity: _ },
231+
),
232+
}
233+
}
218234
}
219235

220236
#[derive(Debug, Default)]
@@ -888,7 +904,29 @@ impl<D: Delegate<Cx = X>, X: Cx> SearchGraph<D> {
888904
!entries.is_empty()
889905
});
890906
}
907+
}
908+
909+
/// We need to rebase provisional cache entries when popping one of their cycle
910+
/// heads from the stack. This may not necessarily mean that we've actually
911+
/// reached a fixpoint for that cycle head, which impacts the way we rebase
912+
/// provisional cache entries.
913+
enum RebaseReason {
914+
NoCycleUsages,
915+
Ambiguity,
916+
Overflow,
917+
/// We've actually reached a fixpoint.
918+
///
919+
/// This either happens in the first evaluation step for the cycle head.
920+
/// In this case the used provisional result depends on the cycle `PathKind`.
921+
/// We store this path kind to check whether the the provisional cache entry
922+
/// we're rebasing relied on the same cycles.
923+
///
924+
/// In later iterations cycles always return `stack_entry.provisional_result`
925+
/// so we no longer depend on the `PathKind`. We store `None` in that case.
926+
ReachedFixpoint(Option<PathKind>),
927+
}
891928

929+
impl<D: Delegate<Cx = X>, X: Cx> SearchGraph<D, X> {
892930
/// A necessary optimization to handle complex solver cycles. A provisional cache entry
893931
/// relies on a set of cycle heads and the path towards these heads. When popping a cycle
894932
/// head from the stack after we've finished computing it, we can't be sure that the
@@ -908,8 +946,9 @@ impl<D: Delegate<Cx = X>, X: Cx> SearchGraph<D> {
908946
/// to me.
909947
fn rebase_provisional_cache_entries(
910948
&mut self,
949+
cx: X,
911950
stack_entry: &StackEntry<X>,
912-
mut mutate_result: impl FnMut(X::Input, X::Result) -> X::Result,
951+
rebase_reason: RebaseReason,
913952
) {
914953
let popped_head_index = self.stack.next_index();
915954
#[allow(rustc::potential_query_instability)]
@@ -927,6 +966,10 @@ impl<D: Delegate<Cx = X>, X: Cx> SearchGraph<D> {
927966
return true;
928967
};
929968

969+
let Some(new_highest_head_index) = heads.opt_highest_cycle_head_index() else {
970+
return false;
971+
};
972+
930973
// We're rebasing an entry `e` over a head `p`. This head
931974
// has a number of own heads `h` it depends on.
932975
//
@@ -977,22 +1020,37 @@ impl<D: Delegate<Cx = X>, X: Cx> SearchGraph<D> {
9771020
let eph = ep.extend_with_paths(ph);
9781021
heads.insert(head_index, eph, head.usages);
9791022
}
980-
}
9811023

982-
let Some(head_index) = heads.opt_highest_cycle_head_index() else {
983-
return false;
984-
};
1024+
// The provisional cache entry does depend on the provisional result
1025+
// of the popped cycle head. We need to mutate the result of our
1026+
// provisional cache entry in case we did not reach a fixpoint.
1027+
match rebase_reason {
1028+
// If the cycle head does not actually depend on itself, then
1029+
// the provisional result used by the provisional cache entry
1030+
// is not actually equal to the final provisional result. We
1031+
// need to discard the provisional cache entry in this case.
1032+
RebaseReason::NoCycleUsages => return false,
1033+
RebaseReason::Ambiguity => {
1034+
*result = D::propagate_ambiguity(cx, input, *result);
1035+
}
1036+
RebaseReason::Overflow => *result = D::on_fixpoint_overflow(cx, input),
1037+
RebaseReason::ReachedFixpoint(None) => {}
1038+
RebaseReason::ReachedFixpoint(Some(path_kind)) => {
1039+
if !popped_head.usages.is_single(path_kind) {
1040+
return false;
1041+
}
1042+
}
1043+
};
1044+
}
9851045

9861046
// We now care about the path from the next highest cycle head to the
9871047
// provisional cache entry.
9881048
*path_from_head = path_from_head.extend(Self::cycle_path_kind(
9891049
&self.stack,
9901050
stack_entry.step_kind_from_parent,
991-
head_index,
1051+
new_highest_head_index,
9921052
));
993-
// Mutate the result of the provisional cache entry in case we did
994-
// not reach a fixpoint.
995-
*result = mutate_result(input, *result);
1053+
9961054
true
9971055
});
9981056
!entries.is_empty()
@@ -1209,33 +1267,19 @@ impl<D: Delegate<Cx = X>, X: Cx> SearchGraph<D> {
12091267
/// Whether we've reached a fixpoint when evaluating a cycle head.
12101268
fn reached_fixpoint(
12111269
&mut self,
1212-
cx: X,
12131270
stack_entry: &StackEntry<X>,
12141271
usages: HeadUsages,
12151272
result: X::Result,
1216-
) -> bool {
1273+
) -> Result<Option<PathKind>, ()> {
12171274
let provisional_result = stack_entry.provisional_result;
1218-
if usages.is_empty() {
1219-
true
1220-
} else if let Some(provisional_result) = provisional_result {
1221-
provisional_result == result
1275+
if let Some(provisional_result) = provisional_result {
1276+
if provisional_result == result { Ok(None) } else { Err(()) }
1277+
} else if let Some(path_kind) = D::is_initial_provisional_result(result)
1278+
.filter(|&path_kind| usages.is_single(path_kind))
1279+
{
1280+
Ok(Some(path_kind))
12221281
} else {
1223-
let check = |k| D::is_initial_provisional_result(cx, k, stack_entry.input, result);
1224-
match usages {
1225-
HeadUsages { inductive: _, unknown: 0, coinductive: 0, forced_ambiguity: 0 } => {
1226-
check(PathKind::Inductive)
1227-
}
1228-
HeadUsages { inductive: 0, unknown: _, coinductive: 0, forced_ambiguity: 0 } => {
1229-
check(PathKind::Unknown)
1230-
}
1231-
HeadUsages { inductive: 0, unknown: 0, coinductive: _, forced_ambiguity: 0 } => {
1232-
check(PathKind::Coinductive)
1233-
}
1234-
HeadUsages { inductive: 0, unknown: 0, coinductive: 0, forced_ambiguity: _ } => {
1235-
check(PathKind::ForcedAmbiguity)
1236-
}
1237-
_ => false,
1238-
}
1282+
Err(())
12391283
}
12401284
}
12411285

@@ -1280,8 +1324,19 @@ impl<D: Delegate<Cx = X>, X: Cx> SearchGraph<D> {
12801324
// is equal to the provisional result of the previous iteration, or because
12811325
// this was only the head of either coinductive or inductive cycles, and the
12821326
// final result is equal to the initial response for that case.
1283-
if self.reached_fixpoint(cx, &stack_entry, usages, result) {
1284-
self.rebase_provisional_cache_entries(&stack_entry, |_, result| result);
1327+
if let Ok(fixpoint) = self.reached_fixpoint(&stack_entry, usages, result) {
1328+
self.rebase_provisional_cache_entries(
1329+
cx,
1330+
&stack_entry,
1331+
RebaseReason::ReachedFixpoint(fixpoint),
1332+
);
1333+
return EvaluationResult::finalize(stack_entry, encountered_overflow, result);
1334+
} else if usages.is_empty() {
1335+
self.rebase_provisional_cache_entries(
1336+
cx,
1337+
&stack_entry,
1338+
RebaseReason::NoCycleUsages,
1339+
);
12851340
return EvaluationResult::finalize(stack_entry, encountered_overflow, result);
12861341
}
12871342

@@ -1298,9 +1353,7 @@ impl<D: Delegate<Cx = X>, X: Cx> SearchGraph<D> {
12981353
// we also taint all provisional cache entries which depend on the
12991354
// current goal.
13001355
if D::is_ambiguous_result(result) {
1301-
self.rebase_provisional_cache_entries(&stack_entry, |input, _| {
1302-
D::propagate_ambiguity(cx, input, result)
1303-
});
1356+
self.rebase_provisional_cache_entries(cx, &stack_entry, RebaseReason::Ambiguity);
13041357
return EvaluationResult::finalize(stack_entry, encountered_overflow, result);
13051358
};
13061359

@@ -1310,9 +1363,7 @@ impl<D: Delegate<Cx = X>, X: Cx> SearchGraph<D> {
13101363
if i >= D::FIXPOINT_STEP_LIMIT {
13111364
debug!("canonical cycle overflow");
13121365
let result = D::on_fixpoint_overflow(cx, input);
1313-
self.rebase_provisional_cache_entries(&stack_entry, |input, _| {
1314-
D::on_fixpoint_overflow(cx, input)
1315-
});
1366+
self.rebase_provisional_cache_entries(cx, &stack_entry, RebaseReason::Overflow);
13161367
return EvaluationResult::finalize(stack_entry, encountered_overflow, result);
13171368
}
13181369

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//@ compile-flags: -Znext-solver
2+
//@ check-pass
3+
4+
// A regression test for trait-system-refactor-initiative#232. We've
5+
// previously incorrectly rebased provisional cache entries even if
6+
// the cycle head didn't reach a fixpoint as it did not depend on any
7+
// cycles itself.
8+
//
9+
// Just because the result of a goal does not depend on its own provisional
10+
// result, it does not mean its nested goals don't depend on its result.
11+
struct B;
12+
struct C;
13+
struct D;
14+
15+
pub trait Trait {
16+
type Output;
17+
}
18+
macro_rules! k {
19+
($t:ty) => {
20+
<$t as Trait>::Output
21+
};
22+
}
23+
24+
trait CallB<T1, T2> {
25+
type Output;
26+
type Return;
27+
}
28+
29+
trait CallC<T1> {
30+
type Output;
31+
type Return;
32+
}
33+
34+
trait CallD<T1, T2> {
35+
type Output;
36+
}
37+
38+
fn foo<X, Y>()
39+
where
40+
X: Trait,
41+
Y: Trait,
42+
D: CallD<k![X], k![Y]>,
43+
C: CallC<<D as CallD<k![X], k![Y]>>::Output>,
44+
<C as CallC<<D as CallD<k![X], k![Y]>>::Output>>::Output: Trait,
45+
B: CallB<
46+
<C as CallC<<D as CallD<k![X], k![Y]>>::Output>>::Return,
47+
<C as CallC<<D as CallD<k![X], k![Y]>>::Output>>::Output,
48+
>,
49+
<B as CallB<
50+
<C as CallC<<D as CallD<k![X], k![Y]>>::Output>>::Return,
51+
<C as CallC<<D as CallD<k![X], k![Y]>>::Output>>::Output,
52+
>>::Output: Trait<Output = ()>,
53+
{
54+
}
55+
fn main() {}

0 commit comments

Comments
 (0)