Skip to content

Commit 2ebf407

Browse files
authored
Rollup merge of #136824 - lcnr:yeet, r=compiler-errors
solver cycles are coinductive once they have one coinductive step Implements the new cycle semantics in the new solver, dealing with the fallout from rust-lang/trait-system-refactor-initiative#10. The first commit has been extensively fuzzed via https://github.com/lcnr/search_graph_fuzz. A trait solver cycle is now coinductive if it has at least one *coinductive step*. A step is only considered coinductive if it's a where-clause of an impl of a coinductive trait. The only coinductive traits are `Sized` and auto traits. This differs from the current stable because where a cycle had to consist of exclusively coinductive goals. This is overly limiting and wasn't properly enforced as it (mostly) ignored all non-trait goals. A more in-depth explanation of my reasoning can be found in this separate doc: https://gist.github.com/lcnr/c49d887bbd34f5d05c36d1cf7a1bf5a5. A summary: - imagine using dictionary passing style: map where-bounds to additional "dictonary" fn arguments instead of monomorphization - impls are the only source of truth and introduce a *constructor* of the dictionary type - a trait goal holds if mapping its proof tree to dictionary passing style results in a valid corecursive function - a corecursive function is valid if it is guarded: matching on it should result in a constructor in a finite amount of time. This property should recursively hold for all fields of the constructor - a function is guarded if the recursive call is *behind* a constructor - **and** this constructor is not *moved out of*, e.g. by accessing a field of the dictionary - the "not moved out of" condition is difficult to guarantee in general, e.g. for item bounds of associated types. However, there is no way to *move out* of an auto trait as there is no information you can get from *the inside of* an auto trait bound in the trait system - if we encounter a cycle/recursive call which involves an auto trait, we can always convert the proof tree into a non-recursive function which calls a corecursive function whose first step is the construction of the auto trait dict and which only recursively depends on itself (by inlining the original function until they reach the uses of the auto trait) **we can therefore make any cycle during which we step into an auto trait (or `Sized`) impl coinductive** ---- To fix rust-lang/trait-system-refactor-initiative#10 we could go with a more restrictive version which tries to restrict cycles to only allow code already supported on stable, potentially forcing cycles to be ambiguous if they step through an impl-where clause of a non-coinductive trait. `PathKind` should be a strictly ordered set to allow merging paths without worry. We could therefore add another variant `PathKind::ForceUnknown` which is greater than `PathKind::Coinductive`. We already have to add such a third `PathKind` in #137314 anyways. I am not doing this here due to multiple reasons: - I cannot think of a principled reason why cycles using an impl to normalize differ in any way from simply using that impl to prove a trait bound. It feels unnecessary and like it makes it more difficult to reason about our cycle semantics :< - This PR does not affect stable as coherence doesn't care about whether a goal holds or is ambiguous. So we don't yet have to make a final decision r? `@compiler-errors` `@nikomatsakis`
2 parents 743f26d + 6a3b30f commit 2ebf407

40 files changed

+691
-456
lines changed

compiler/rustc_middle/src/ty/context.rs

+4
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,10 @@ impl<'tcx> Interner for TyCtxt<'tcx> {
594594
self.trait_is_auto(trait_def_id)
595595
}
596596

597+
fn trait_is_coinductive(self, trait_def_id: DefId) -> bool {
598+
self.trait_is_coinductive(trait_def_id)
599+
}
600+
597601
fn trait_is_alias(self, trait_def_id: DefId) -> bool {
598602
self.trait_is_alias(trait_def_id)
599603
}

compiler/rustc_middle/src/ty/predicate.rs

-16
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ use rustc_data_structures::intern::Interned;
44
use rustc_hir::def_id::DefId;
55
use rustc_macros::{HashStable, extension};
66
use rustc_type_ir as ir;
7-
use tracing::instrument;
87

98
use crate::ty::{
109
self, DebruijnIndex, EarlyBinder, PredicatePolarity, Ty, TyCtxt, TypeFlags, Upcast, UpcastFrom,
@@ -51,10 +50,6 @@ impl<'tcx> rustc_type_ir::inherent::Predicate<TyCtxt<'tcx>> for Predicate<'tcx>
5150
self.as_clause()
5251
}
5352

54-
fn is_coinductive(self, interner: TyCtxt<'tcx>) -> bool {
55-
self.is_coinductive(interner)
56-
}
57-
5853
fn allow_normalization(self) -> bool {
5954
self.allow_normalization()
6055
}
@@ -119,17 +114,6 @@ impl<'tcx> Predicate<'tcx> {
119114
Some(tcx.mk_predicate(kind))
120115
}
121116

122-
#[instrument(level = "debug", skip(tcx), ret)]
123-
pub fn is_coinductive(self, tcx: TyCtxt<'tcx>) -> bool {
124-
match self.kind().skip_binder() {
125-
ty::PredicateKind::Clause(ty::ClauseKind::Trait(data)) => {
126-
tcx.trait_is_coinductive(data.def_id())
127-
}
128-
ty::PredicateKind::Clause(ty::ClauseKind::WellFormed(_)) => true,
129-
_ => false,
130-
}
131-
}
132-
133117
/// Whether this projection can be soundly normalized.
134118
///
135119
/// Wf predicates must not be normalized, as normalization

compiler/rustc_next_trait_solver/src/solve/eval_ctxt/canonical.rs

+33-24
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use tracing::{debug, instrument, trace};
2121
use crate::canonicalizer::Canonicalizer;
2222
use crate::delegate::SolverDelegate;
2323
use crate::resolve::EagerResolver;
24-
use crate::solve::eval_ctxt::NestedGoals;
24+
use crate::solve::eval_ctxt::{CurrentGoalKind, NestedGoals};
2525
use crate::solve::{
2626
CanonicalInput, CanonicalResponse, Certainty, EvalCtxt, ExternalConstraintsData, Goal,
2727
MaybeCause, NestedNormalizationGoals, NoSolution, PredefinedOpaquesData, QueryInput,
@@ -109,18 +109,22 @@ where
109109
//
110110
// As we return all ambiguous nested goals, we can ignore the certainty returned
111111
// by `try_evaluate_added_goals()`.
112-
let (certainty, normalization_nested_goals) = if self.is_normalizes_to_goal {
113-
let NestedGoals { normalizes_to_goals, goals } = std::mem::take(&mut self.nested_goals);
114-
if cfg!(debug_assertions) {
115-
assert!(normalizes_to_goals.is_empty());
116-
if goals.is_empty() {
117-
assert!(matches!(goals_certainty, Certainty::Yes));
112+
let (certainty, normalization_nested_goals) = match self.current_goal_kind {
113+
CurrentGoalKind::NormalizesTo => {
114+
let NestedGoals { normalizes_to_goals, goals } =
115+
std::mem::take(&mut self.nested_goals);
116+
if cfg!(debug_assertions) {
117+
assert!(normalizes_to_goals.is_empty());
118+
if goals.is_empty() {
119+
assert!(matches!(goals_certainty, Certainty::Yes));
120+
}
118121
}
122+
(certainty, NestedNormalizationGoals(goals))
123+
}
124+
CurrentGoalKind::Misc | CurrentGoalKind::CoinductiveTrait => {
125+
let certainty = certainty.unify_with(goals_certainty);
126+
(certainty, NestedNormalizationGoals::empty())
119127
}
120-
(certainty, NestedNormalizationGoals(goals))
121-
} else {
122-
let certainty = certainty.unify_with(goals_certainty);
123-
(certainty, NestedNormalizationGoals::empty())
124128
};
125129

126130
if let Certainty::Maybe(cause @ MaybeCause::Overflow { .. }) = certainty {
@@ -163,19 +167,24 @@ where
163167
// ambiguous alias types which get replaced with fresh inference variables
164168
// during generalization. This prevents hangs caused by an exponential blowup,
165169
// see tests/ui/traits/next-solver/coherence-alias-hang.rs.
166-
//
167-
// We don't do so for `NormalizesTo` goals as we erased the expected term and
168-
// bailing with overflow here would prevent us from detecting a type-mismatch,
169-
// causing a coherence error in diesel, see #131969. We still bail with overflow
170-
// when later returning from the parent AliasRelate goal.
171-
if !self.is_normalizes_to_goal {
172-
let num_non_region_vars =
173-
canonical.variables.iter().filter(|c| !c.is_region() && c.is_existential()).count();
174-
if num_non_region_vars > self.cx().recursion_limit() {
175-
debug!(?num_non_region_vars, "too many inference variables -> overflow");
176-
return Ok(self.make_ambiguous_response_no_constraints(MaybeCause::Overflow {
177-
suggest_increasing_limit: true,
178-
}));
170+
match self.current_goal_kind {
171+
// We don't do so for `NormalizesTo` goals as we erased the expected term and
172+
// bailing with overflow here would prevent us from detecting a type-mismatch,
173+
// causing a coherence error in diesel, see #131969. We still bail with overflow
174+
// when later returning from the parent AliasRelate goal.
175+
CurrentGoalKind::NormalizesTo => {}
176+
CurrentGoalKind::Misc | CurrentGoalKind::CoinductiveTrait => {
177+
let num_non_region_vars = canonical
178+
.variables
179+
.iter()
180+
.filter(|c| !c.is_region() && c.is_existential())
181+
.count();
182+
if num_non_region_vars > self.cx().recursion_limit() {
183+
debug!(?num_non_region_vars, "too many inference variables -> overflow");
184+
return Ok(self.make_ambiguous_response_no_constraints(MaybeCause::Overflow {
185+
suggest_increasing_limit: true,
186+
}));
187+
}
179188
}
180189
}
181190

compiler/rustc_next_trait_solver/src/solve/eval_ctxt/mod.rs

+95-23
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use rustc_type_ir::fold::{TypeFoldable, TypeFolder, TypeSuperFoldable};
99
use rustc_type_ir::inherent::*;
1010
use rustc_type_ir::relate::Relate;
1111
use rustc_type_ir::relate::solver_relating::RelateExt;
12+
use rustc_type_ir::search_graph::PathKind;
1213
use rustc_type_ir::visit::{TypeSuperVisitable, TypeVisitable, TypeVisitableExt, TypeVisitor};
1314
use rustc_type_ir::{self as ty, CanonicalVarValues, InferCtxtLike, Interner, TypingMode};
1415
use rustc_type_ir_macros::{Lift_Generic, TypeFoldable_Generic, TypeVisitable_Generic};
@@ -20,12 +21,51 @@ use crate::solve::inspect::{self, ProofTreeBuilder};
2021
use crate::solve::search_graph::SearchGraph;
2122
use crate::solve::{
2223
CanonicalInput, Certainty, FIXPOINT_STEP_LIMIT, Goal, GoalEvaluationKind, GoalSource,
23-
HasChanged, NestedNormalizationGoals, NoSolution, PredefinedOpaquesData, QueryResult,
24+
HasChanged, NestedNormalizationGoals, NoSolution, PredefinedOpaquesData, QueryInput,
25+
QueryResult,
2426
};
2527

2628
pub(super) mod canonical;
2729
mod probe;
2830

31+
/// The kind of goal we're currently proving.
32+
///
33+
/// This has effects on cycle handling handling and on how we compute
34+
/// query responses, see the variant descriptions for more info.
35+
#[derive(Debug, Copy, Clone)]
36+
enum CurrentGoalKind {
37+
Misc,
38+
/// We're proving an trait goal for a coinductive trait, either an auto trait or `Sized`.
39+
///
40+
/// These are currently the only goals whose impl where-clauses are considered to be
41+
/// productive steps.
42+
CoinductiveTrait,
43+
/// Unlike other goals, `NormalizesTo` goals act like functions with the expected term
44+
/// always being fully unconstrained. This would weaken inference however, as the nested
45+
/// goals never get the inference constraints from the actual normalized-to type.
46+
///
47+
/// Because of this we return any ambiguous nested goals from `NormalizesTo` to the
48+
/// caller when then adds these to its own context. The caller is always an `AliasRelate`
49+
/// goal so this never leaks out of the solver.
50+
NormalizesTo,
51+
}
52+
53+
impl CurrentGoalKind {
54+
fn from_query_input<I: Interner>(cx: I, input: QueryInput<I, I::Predicate>) -> CurrentGoalKind {
55+
match input.goal.predicate.kind().skip_binder() {
56+
ty::PredicateKind::Clause(ty::ClauseKind::Trait(pred)) => {
57+
if cx.trait_is_coinductive(pred.trait_ref.def_id) {
58+
CurrentGoalKind::CoinductiveTrait
59+
} else {
60+
CurrentGoalKind::Misc
61+
}
62+
}
63+
ty::PredicateKind::NormalizesTo(_) => CurrentGoalKind::NormalizesTo,
64+
_ => CurrentGoalKind::Misc,
65+
}
66+
}
67+
}
68+
2969
pub struct EvalCtxt<'a, D, I = <D as SolverDelegate>::Interner>
3070
where
3171
D: SolverDelegate<Interner = I>,
@@ -51,14 +91,10 @@ where
5191
/// The variable info for the `var_values`, only used to make an ambiguous response
5292
/// with no constraints.
5393
variables: I::CanonicalVars,
54-
/// Whether we're currently computing a `NormalizesTo` goal. Unlike other goals,
55-
/// `NormalizesTo` goals act like functions with the expected term always being
56-
/// fully unconstrained. This would weaken inference however, as the nested goals
57-
/// never get the inference constraints from the actual normalized-to type. Because
58-
/// of this we return any ambiguous nested goals from `NormalizesTo` to the caller
59-
/// when then adds these to its own context. The caller is always an `AliasRelate`
60-
/// goal so this never leaks out of the solver.
61-
is_normalizes_to_goal: bool,
94+
95+
/// What kind of goal we're currently computing, see the enum definition
96+
/// for more info.
97+
current_goal_kind: CurrentGoalKind,
6298
pub(super) var_values: CanonicalVarValues<I>,
6399

64100
predefined_opaques_in_body: I::PredefinedOpaques,
@@ -226,8 +262,22 @@ where
226262
self.delegate.typing_mode()
227263
}
228264

229-
pub(super) fn set_is_normalizes_to_goal(&mut self) {
230-
self.is_normalizes_to_goal = true;
265+
/// Computes the `PathKind` for the step from the current goal to the
266+
/// nested goal required due to `source`.
267+
///
268+
/// See #136824 for a more detailed reasoning for this behavior. We
269+
/// consider cycles to be coinductive if they 'step into' a where-clause
270+
/// of a coinductive trait. We will likely extend this function in the future
271+
/// and will need to clearly document it in the rustc-dev-guide before
272+
/// stabilization.
273+
pub(super) fn step_kind_for_source(&self, source: GoalSource) -> PathKind {
274+
match (self.current_goal_kind, source) {
275+
(_, GoalSource::NormalizeGoal(step_kind)) => step_kind,
276+
(CurrentGoalKind::CoinductiveTrait, GoalSource::ImplWhereBound) => {
277+
PathKind::Coinductive
278+
}
279+
_ => PathKind::Inductive,
280+
}
231281
}
232282

233283
/// Creates a root evaluation context and search graph. This should only be
@@ -256,7 +306,7 @@ where
256306
max_input_universe: ty::UniverseIndex::ROOT,
257307
variables: Default::default(),
258308
var_values: CanonicalVarValues::dummy(),
259-
is_normalizes_to_goal: false,
309+
current_goal_kind: CurrentGoalKind::Misc,
260310
origin_span,
261311
tainted: Ok(()),
262312
};
@@ -294,7 +344,7 @@ where
294344
delegate,
295345
variables: canonical_input.canonical.variables,
296346
var_values,
297-
is_normalizes_to_goal: false,
347+
current_goal_kind: CurrentGoalKind::from_query_input(cx, input),
298348
predefined_opaques_in_body: input.predefined_opaques_in_body,
299349
max_input_universe: canonical_input.canonical.max_universe,
300350
search_graph,
@@ -340,6 +390,7 @@ where
340390
cx: I,
341391
search_graph: &'a mut SearchGraph<D>,
342392
canonical_input: CanonicalInput<I>,
393+
step_kind_from_parent: PathKind,
343394
goal_evaluation: &mut ProofTreeBuilder<D>,
344395
) -> QueryResult<I> {
345396
let mut canonical_goal_evaluation =
@@ -352,6 +403,7 @@ where
352403
search_graph.with_new_goal(
353404
cx,
354405
canonical_input,
406+
step_kind_from_parent,
355407
&mut canonical_goal_evaluation,
356408
|search_graph, canonical_goal_evaluation| {
357409
EvalCtxt::enter_canonical(
@@ -395,12 +447,10 @@ where
395447
/// `NormalizesTo` is only used by `AliasRelate`, all other callsites
396448
/// should use [`EvalCtxt::evaluate_goal`] which discards that empty
397449
/// storage.
398-
// FIXME(-Znext-solver=coinduction): `_source` is currently unused but will
399-
// be necessary once we implement the new coinduction approach.
400450
pub(super) fn evaluate_goal_raw(
401451
&mut self,
402452
goal_evaluation_kind: GoalEvaluationKind,
403-
_source: GoalSource,
453+
source: GoalSource,
404454
goal: Goal<I, I::Predicate>,
405455
) -> Result<(NestedNormalizationGoals<I>, HasChanged, Certainty), NoSolution> {
406456
let (orig_values, canonical_goal) = self.canonicalize_goal(goal);
@@ -410,6 +460,7 @@ where
410460
self.cx(),
411461
self.search_graph,
412462
canonical_goal,
463+
self.step_kind_for_source(source),
413464
&mut goal_evaluation,
414465
);
415466
let response = match canonical_response {
@@ -630,16 +681,19 @@ where
630681

631682
#[instrument(level = "trace", skip(self))]
632683
pub(super) fn add_normalizes_to_goal(&mut self, mut goal: Goal<I, ty::NormalizesTo<I>>) {
633-
goal.predicate =
634-
goal.predicate.fold_with(&mut ReplaceAliasWithInfer::new(self, goal.param_env));
684+
goal.predicate = goal.predicate.fold_with(&mut ReplaceAliasWithInfer::new(
685+
self,
686+
GoalSource::Misc,
687+
goal.param_env,
688+
));
635689
self.inspect.add_normalizes_to_goal(self.delegate, self.max_input_universe, goal);
636690
self.nested_goals.normalizes_to_goals.push(goal);
637691
}
638692

639693
#[instrument(level = "debug", skip(self))]
640694
pub(super) fn add_goal(&mut self, source: GoalSource, mut goal: Goal<I, I::Predicate>) {
641695
goal.predicate =
642-
goal.predicate.fold_with(&mut ReplaceAliasWithInfer::new(self, goal.param_env));
696+
goal.predicate.fold_with(&mut ReplaceAliasWithInfer::new(self, source, goal.param_env));
643697
self.inspect.add_goal(self.delegate, self.max_input_universe, source, goal);
644698
self.nested_goals.goals.push((source, goal));
645699
}
@@ -1053,13 +1107,21 @@ where
10531107
///
10541108
/// This is a performance optimization to more eagerly detect cycles during trait
10551109
/// solving. See tests/ui/traits/next-solver/cycles/cycle-modulo-ambig-aliases.rs.
1110+
///
1111+
/// The emitted goals get evaluated in the context of the parent goal; by
1112+
/// replacing aliases in nested goals we essentially pull the normalization out of
1113+
/// the nested goal. We want to treat the goal as if the normalization still happens
1114+
/// inside of the nested goal by inheriting the `step_kind` of the nested goal and
1115+
/// storing it in the `GoalSource` of the emitted `AliasRelate` goals.
1116+
/// This is necessary for tests/ui/sized/coinductive-1.rs to compile.
10561117
struct ReplaceAliasWithInfer<'me, 'a, D, I>
10571118
where
10581119
D: SolverDelegate<Interner = I>,
10591120
I: Interner,
10601121
{
10611122
ecx: &'me mut EvalCtxt<'a, D>,
10621123
param_env: I::ParamEnv,
1124+
normalization_goal_source: GoalSource,
10631125
cache: HashMap<I::Ty, I::Ty>,
10641126
}
10651127

@@ -1068,8 +1130,18 @@ where
10681130
D: SolverDelegate<Interner = I>,
10691131
I: Interner,
10701132
{
1071-
fn new(ecx: &'me mut EvalCtxt<'a, D>, param_env: I::ParamEnv) -> Self {
1072-
ReplaceAliasWithInfer { ecx, param_env, cache: Default::default() }
1133+
fn new(
1134+
ecx: &'me mut EvalCtxt<'a, D>,
1135+
for_goal_source: GoalSource,
1136+
param_env: I::ParamEnv,
1137+
) -> Self {
1138+
let step_kind = ecx.step_kind_for_source(for_goal_source);
1139+
ReplaceAliasWithInfer {
1140+
ecx,
1141+
param_env,
1142+
normalization_goal_source: GoalSource::NormalizeGoal(step_kind),
1143+
cache: Default::default(),
1144+
}
10731145
}
10741146
}
10751147

@@ -1092,7 +1164,7 @@ where
10921164
ty::AliasRelationDirection::Equate,
10931165
);
10941166
self.ecx.add_goal(
1095-
GoalSource::Misc,
1167+
self.normalization_goal_source,
10961168
Goal::new(self.cx(), self.param_env, normalizes_to),
10971169
);
10981170
infer_ty
@@ -1121,7 +1193,7 @@ where
11211193
ty::AliasRelationDirection::Equate,
11221194
);
11231195
self.ecx.add_goal(
1124-
GoalSource::Misc,
1196+
self.normalization_goal_source,
11251197
Goal::new(self.cx(), self.param_env, normalizes_to),
11261198
);
11271199
infer_ct

compiler/rustc_next_trait_solver/src/solve/eval_ctxt/probe.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ where
3434
delegate,
3535
variables: outer_ecx.variables,
3636
var_values: outer_ecx.var_values,
37-
is_normalizes_to_goal: outer_ecx.is_normalizes_to_goal,
37+
current_goal_kind: outer_ecx.current_goal_kind,
3838
predefined_opaques_in_body: outer_ecx.predefined_opaques_in_body,
3939
max_input_universe,
4040
search_graph: outer_ecx.search_graph,

compiler/rustc_next_trait_solver/src/solve/normalizes_to/inherent.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ where
3939
//
4040
// FIXME(-Znext-solver=coinductive): I think this should be split
4141
// and we tag the impl bounds with `GoalSource::ImplWhereBound`?
42-
// Right not this includes both the impl and the assoc item where bounds,
42+
// Right now this includes both the impl and the assoc item where bounds,
4343
// and I don't think the assoc item where-bounds are allowed to be coinductive.
4444
self.add_goals(
4545
GoalSource::Misc,

compiler/rustc_next_trait_solver/src/solve/normalizes_to/mod.rs

-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ where
2828
&mut self,
2929
goal: Goal<I, NormalizesTo<I>>,
3030
) -> QueryResult<I> {
31-
self.set_is_normalizes_to_goal();
3231
debug_assert!(self.term_is_fully_unconstrained(goal));
3332
let cx = self.cx();
3433
match goal.predicate.alias.kind(cx) {

0 commit comments

Comments
 (0)