Skip to content

Commit a32978a

Browse files
committed
Auto merge of #114023 - compiler-errors:coinductive-cycle-lint, r=lcnr
Warn on inductive cycle in coherence leading to impls being considered not overlapping This PR implements a `coinductive_overlap_in_coherence` lint (#114040), which warns users against cases where two impls are considered **not** to overlap during coherence due to an inductive cycle disproving one of the predicates after unifying the two impls. Cases where this lint fires will become an overlap error if we ever move to coinduction, so I'd like to make this a warning to avoid having more crates take advantage of this behavior in the mean time. Also, since the new trait solver treats inductive cycles as ambiguity, not an error, this is a blocker for landing the new trait solver in coherence.
2 parents d7e7510 + 0e20155 commit a32978a

File tree

10 files changed

+252
-54
lines changed

10 files changed

+252
-54
lines changed

compiler/rustc_lint_defs/src/builtin.rs

+39
Original file line numberDiff line numberDiff line change
@@ -3366,6 +3366,7 @@ declare_lint_pass! {
33663366
BYTE_SLICE_IN_PACKED_STRUCT_WITH_DERIVE,
33673367
CENUM_IMPL_DROP_CAST,
33683368
COHERENCE_LEAK_CHECK,
3369+
COINDUCTIVE_OVERLAP_IN_COHERENCE,
33693370
CONFLICTING_REPR_HINTS,
33703371
CONST_EVALUATABLE_UNCHECKED,
33713372
CONST_ITEM_MUTATION,
@@ -4422,6 +4423,44 @@ declare_lint! {
44224423
@feature_gate = sym::type_privacy_lints;
44234424
}
44244425

4426+
declare_lint! {
4427+
/// The `coinductive_overlap_in_coherence` lint detects impls which are currently
4428+
/// considered not overlapping, but may be considered to overlap if support for
4429+
/// coinduction is added to the trait solver.
4430+
///
4431+
/// ### Example
4432+
///
4433+
/// ```rust,compile_fail
4434+
/// #![deny(coinductive_overlap_in_coherence)]
4435+
///
4436+
/// trait CyclicTrait {}
4437+
/// impl<T: CyclicTrait> CyclicTrait for T {}
4438+
///
4439+
/// trait Trait {}
4440+
/// impl<T: CyclicTrait> Trait for T {}
4441+
/// // conflicting impl with the above
4442+
/// impl Trait for u8 {}
4443+
/// ```
4444+
///
4445+
/// {{produces}}
4446+
///
4447+
/// ### Explanation
4448+
///
4449+
/// We have two choices for impl which satisfy `u8: Trait`: the blanket impl
4450+
/// for generic `T`, and the direct impl for `u8`. These two impls nominally
4451+
/// overlap, since we can infer `T = u8` in the former impl, but since the where
4452+
/// clause `u8: CyclicTrait` would end up resulting in a cycle (since it depends
4453+
/// on itself), the blanket impl is not considered to hold for `u8`. This will
4454+
/// change in a future release.
4455+
pub COINDUCTIVE_OVERLAP_IN_COHERENCE,
4456+
Warn,
4457+
"impls that are not considered to overlap may be considered to \
4458+
overlap in the future",
4459+
@future_incompatible = FutureIncompatibleInfo {
4460+
reference: "issue #114040 <https://github.com/rust-lang/rust/issues/114040>",
4461+
};
4462+
}
4463+
44254464
declare_lint! {
44264465
/// The `unknown_diagnostic_attributes` lint detects unrecognized diagnostic attributes.
44274466
///

compiler/rustc_middle/src/ty/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ pub struct ImplHeader<'tcx> {
238238
pub impl_def_id: DefId,
239239
pub self_ty: Ty<'tcx>,
240240
pub trait_ref: Option<TraitRef<'tcx>>,
241-
pub predicates: Vec<Predicate<'tcx>>,
241+
pub predicates: Vec<(Predicate<'tcx>, Span)>,
242242
}
243243

244244
#[derive(Copy, Clone, PartialEq, Eq, Debug, TypeFoldable, TypeVisitable)]

compiler/rustc_trait_selection/src/traits/coherence.rs

+97-44
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
use crate::infer::outlives::env::OutlivesEnvironment;
88
use crate::infer::InferOk;
99
use crate::traits::outlives_bounds::InferCtxtExt as _;
10-
use crate::traits::select::IntercrateAmbiguityCause;
10+
use crate::traits::select::{IntercrateAmbiguityCause, TreatInductiveCycleAs};
1111
use crate::traits::util::impl_subject_and_oblig;
1212
use crate::traits::SkipLeakCheck;
1313
use crate::traits::{
@@ -24,6 +24,7 @@ use rustc_middle::traits::DefiningAnchor;
2424
use rustc_middle::ty::fast_reject::{DeepRejectCtxt, TreatParams};
2525
use rustc_middle::ty::visit::{TypeVisitable, TypeVisitableExt};
2626
use rustc_middle::ty::{self, Ty, TyCtxt, TypeVisitor};
27+
use rustc_session::lint::builtin::COINDUCTIVE_OVERLAP_IN_COHERENCE;
2728
use rustc_span::symbol::sym;
2829
use rustc_span::DUMMY_SP;
2930
use std::fmt::Debug;
@@ -151,14 +152,16 @@ fn with_fresh_ty_vars<'cx, 'tcx>(
151152
.predicates_of(impl_def_id)
152153
.instantiate(tcx, impl_args)
153154
.iter()
154-
.map(|(c, _)| c.as_predicate())
155+
.map(|(c, s)| (c.as_predicate(), s))
155156
.collect(),
156157
};
157158

158-
let InferOk { value: mut header, obligations } =
159-
selcx.infcx.at(&ObligationCause::dummy(), param_env).normalize(header);
159+
let InferOk { value: mut header, obligations } = selcx
160+
.infcx
161+
.at(&ObligationCause::dummy_with_span(tcx.def_span(impl_def_id)), param_env)
162+
.normalize(header);
160163

161-
header.predicates.extend(obligations.into_iter().map(|o| o.predicate));
164+
header.predicates.extend(obligations.into_iter().map(|o| (o.predicate, o.cause.span)));
162165
header
163166
}
164167

@@ -207,16 +210,76 @@ fn overlap<'tcx>(
207210
let equate_obligations = equate_impl_headers(selcx.infcx, &impl1_header, &impl2_header)?;
208211
debug!("overlap: unification check succeeded");
209212

210-
if overlap_mode.use_implicit_negative()
211-
&& impl_intersection_has_impossible_obligation(
212-
selcx,
213-
param_env,
214-
&impl1_header,
215-
impl2_header,
216-
equate_obligations,
217-
)
218-
{
219-
return None;
213+
if overlap_mode.use_implicit_negative() {
214+
for mode in [TreatInductiveCycleAs::Ambig, TreatInductiveCycleAs::Recur] {
215+
if let Some(failing_obligation) = selcx.with_treat_inductive_cycle_as(mode, |selcx| {
216+
impl_intersection_has_impossible_obligation(
217+
selcx,
218+
param_env,
219+
&impl1_header,
220+
&impl2_header,
221+
&equate_obligations,
222+
)
223+
}) {
224+
if matches!(mode, TreatInductiveCycleAs::Recur) {
225+
let first_local_impl = impl1_header
226+
.impl_def_id
227+
.as_local()
228+
.or(impl2_header.impl_def_id.as_local())
229+
.expect("expected one of the impls to be local");
230+
infcx.tcx.struct_span_lint_hir(
231+
COINDUCTIVE_OVERLAP_IN_COHERENCE,
232+
infcx.tcx.local_def_id_to_hir_id(first_local_impl),
233+
infcx.tcx.def_span(first_local_impl),
234+
format!(
235+
"implementations {} will conflict in the future",
236+
match impl1_header.trait_ref {
237+
Some(trait_ref) => {
238+
let trait_ref = infcx.resolve_vars_if_possible(trait_ref);
239+
format!(
240+
"of `{}` for `{}`",
241+
trait_ref.print_only_trait_path(),
242+
trait_ref.self_ty()
243+
)
244+
}
245+
None => format!(
246+
"for `{}`",
247+
infcx.resolve_vars_if_possible(impl1_header.self_ty)
248+
),
249+
},
250+
),
251+
|lint| {
252+
lint.note(
253+
"impls that are not considered to overlap may be considered to \
254+
overlap in the future",
255+
)
256+
.span_label(
257+
infcx.tcx.def_span(impl1_header.impl_def_id),
258+
"the first impl is here",
259+
)
260+
.span_label(
261+
infcx.tcx.def_span(impl2_header.impl_def_id),
262+
"the second impl is here",
263+
);
264+
if !failing_obligation.cause.span.is_dummy() {
265+
lint.span_label(
266+
failing_obligation.cause.span,
267+
format!(
268+
"`{}` may be considered to hold in future releases, \
269+
causing the impls to overlap",
270+
infcx
271+
.resolve_vars_if_possible(failing_obligation.predicate)
272+
),
273+
);
274+
}
275+
lint
276+
},
277+
);
278+
}
279+
280+
return None;
281+
}
282+
}
220283
}
221284

222285
// We toggle the `leak_check` by using `skip_leak_check` when constructing the
@@ -284,40 +347,30 @@ fn impl_intersection_has_impossible_obligation<'cx, 'tcx>(
284347
selcx: &mut SelectionContext<'cx, 'tcx>,
285348
param_env: ty::ParamEnv<'tcx>,
286349
impl1_header: &ty::ImplHeader<'tcx>,
287-
impl2_header: ty::ImplHeader<'tcx>,
288-
obligations: PredicateObligations<'tcx>,
289-
) -> bool {
350+
impl2_header: &ty::ImplHeader<'tcx>,
351+
obligations: &PredicateObligations<'tcx>,
352+
) -> Option<PredicateObligation<'tcx>> {
290353
let infcx = selcx.infcx;
291354

292-
let obligation_guaranteed_to_fail = move |obligation: &PredicateObligation<'tcx>| {
293-
if infcx.next_trait_solver() {
294-
infcx.evaluate_obligation(obligation).map_or(false, |result| !result.may_apply())
295-
} else {
296-
// We use `evaluate_root_obligation` to correctly track
297-
// intercrate ambiguity clauses. We do not need this in the
298-
// new solver.
299-
selcx.evaluate_root_obligation(obligation).map_or(
300-
false, // Overflow has occurred, and treat the obligation as possibly holding.
301-
|result| !result.may_apply(),
302-
)
303-
}
304-
};
305-
306-
let opt_failing_obligation = [&impl1_header.predicates, &impl2_header.predicates]
355+
[&impl1_header.predicates, &impl2_header.predicates]
307356
.into_iter()
308357
.flatten()
309-
.map(|&predicate| {
310-
Obligation::new(infcx.tcx, ObligationCause::dummy(), param_env, predicate)
358+
.map(|&(predicate, span)| {
359+
Obligation::new(infcx.tcx, ObligationCause::dummy_with_span(span), param_env, predicate)
360+
})
361+
.chain(obligations.into_iter().cloned())
362+
.find(|obligation: &PredicateObligation<'tcx>| {
363+
if infcx.next_trait_solver() {
364+
infcx.evaluate_obligation(obligation).map_or(false, |result| !result.may_apply())
365+
} else {
366+
// We use `evaluate_root_obligation` to correctly track intercrate
367+
// ambiguity clauses. We cannot use this in the new solver.
368+
selcx.evaluate_root_obligation(obligation).map_or(
369+
false, // Overflow has occurred, and treat the obligation as possibly holding.
370+
|result| !result.may_apply(),
371+
)
372+
}
311373
})
312-
.chain(obligations)
313-
.find(obligation_guaranteed_to_fail);
314-
315-
if let Some(failing_obligation) = opt_failing_obligation {
316-
debug!("overlap: obligation unsatisfiable {:?}", failing_obligation);
317-
true
318-
} else {
319-
false
320-
}
321374
}
322375

323376
/// Check if both impls can be satisfied by a common type by considering whether

compiler/rustc_trait_selection/src/traits/select/mod.rs

+43-3
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ pub struct SelectionContext<'cx, 'tcx> {
118118
/// policy. In essence, canonicalized queries need their errors propagated
119119
/// rather than immediately reported because we do not have accurate spans.
120120
query_mode: TraitQueryMode,
121+
122+
treat_inductive_cycle: TreatInductiveCycleAs,
121123
}
122124

123125
// A stack that walks back up the stack frame.
@@ -198,16 +200,54 @@ enum BuiltinImplConditions<'tcx> {
198200
Ambiguous,
199201
}
200202

203+
#[derive(Copy, Clone)]
204+
pub enum TreatInductiveCycleAs {
205+
/// This is the previous behavior, where `Recur` represents an inductive
206+
/// cycle that is known not to hold. This is not forwards-compatible with
207+
/// coinduction, and will be deprecated. This is the default behavior
208+
/// of the old trait solver due to back-compat reasons.
209+
Recur,
210+
/// This is the behavior of the new trait solver, where inductive cycles
211+
/// are treated as ambiguous and possibly holding.
212+
Ambig,
213+
}
214+
215+
impl From<TreatInductiveCycleAs> for EvaluationResult {
216+
fn from(treat: TreatInductiveCycleAs) -> EvaluationResult {
217+
match treat {
218+
TreatInductiveCycleAs::Ambig => EvaluatedToUnknown,
219+
TreatInductiveCycleAs::Recur => EvaluatedToRecur,
220+
}
221+
}
222+
}
223+
201224
impl<'cx, 'tcx> SelectionContext<'cx, 'tcx> {
202225
pub fn new(infcx: &'cx InferCtxt<'tcx>) -> SelectionContext<'cx, 'tcx> {
203226
SelectionContext {
204227
infcx,
205228
freshener: infcx.freshener(),
206229
intercrate_ambiguity_causes: None,
207230
query_mode: TraitQueryMode::Standard,
231+
treat_inductive_cycle: TreatInductiveCycleAs::Recur,
208232
}
209233
}
210234

235+
// Sets the `TreatInductiveCycleAs` mode temporarily in the selection context
236+
pub fn with_treat_inductive_cycle_as<T>(
237+
&mut self,
238+
treat_inductive_cycle: TreatInductiveCycleAs,
239+
f: impl FnOnce(&mut Self) -> T,
240+
) -> T {
241+
// Should be executed in a context where caching is disabled,
242+
// otherwise the cache is poisoned with the temporary result.
243+
assert!(self.is_intercrate());
244+
let treat_inductive_cycle =
245+
std::mem::replace(&mut self.treat_inductive_cycle, treat_inductive_cycle);
246+
let value = f(self);
247+
self.treat_inductive_cycle = treat_inductive_cycle;
248+
value
249+
}
250+
211251
pub fn with_query_mode(
212252
infcx: &'cx InferCtxt<'tcx>,
213253
query_mode: TraitQueryMode,
@@ -719,7 +759,7 @@ impl<'cx, 'tcx> SelectionContext<'cx, 'tcx> {
719759
stack.update_reached_depth(stack_arg.1);
720760
return Ok(EvaluatedToOk);
721761
} else {
722-
return Ok(EvaluatedToRecur);
762+
return Ok(self.treat_inductive_cycle.into());
723763
}
724764
}
725765
return Ok(EvaluatedToOk);
@@ -837,7 +877,7 @@ impl<'cx, 'tcx> SelectionContext<'cx, 'tcx> {
837877
}
838878
}
839879
ProjectAndUnifyResult::FailedNormalization => Ok(EvaluatedToAmbig),
840-
ProjectAndUnifyResult::Recursive => Ok(EvaluatedToRecur),
880+
ProjectAndUnifyResult::Recursive => Ok(self.treat_inductive_cycle.into()),
841881
ProjectAndUnifyResult::MismatchedProjectionTypes(_) => Ok(EvaluatedToErr),
842882
}
843883
}
@@ -1151,7 +1191,7 @@ impl<'cx, 'tcx> SelectionContext<'cx, 'tcx> {
11511191
Some(EvaluatedToOk)
11521192
} else {
11531193
debug!("evaluate_stack --> recursive, inductive");
1154-
Some(EvaluatedToRecur)
1194+
Some(self.treat_inductive_cycle.into())
11551195
}
11561196
} else {
11571197
None
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#![deny(coinductive_overlap_in_coherence)]
2+
3+
use std::borrow::Borrow;
4+
use std::cmp::Ordering;
5+
use std::marker::PhantomData;
6+
7+
#[derive(PartialEq, Default)]
8+
pub(crate) struct Interval<T>(PhantomData<T>);
9+
10+
// This impl overlaps with the `derive` unless we reject the nested
11+
// `Interval<?1>: PartialOrd<Interval<?1>>` candidate which results
12+
// in a - currently inductive - cycle.
13+
impl<T, Q> PartialEq<Q> for Interval<T>
14+
//~^ ERROR implementations of `PartialEq<Interval<_>>` for `Interval<_>` will conflict in the future
15+
//~| WARN this was previously accepted by the compiler but is being phased out
16+
where
17+
T: Borrow<Q>,
18+
Q: ?Sized + PartialOrd,
19+
{
20+
fn eq(&self, _: &Q) -> bool {
21+
true
22+
}
23+
}
24+
25+
impl<T, Q> PartialOrd<Q> for Interval<T>
26+
where
27+
T: Borrow<Q>,
28+
Q: ?Sized + PartialOrd,
29+
{
30+
fn partial_cmp(&self, _: &Q) -> Option<Ordering> {
31+
None
32+
}
33+
}
34+
35+
fn main() {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
error: implementations of `PartialEq<Interval<_>>` for `Interval<_>` will conflict in the future
2+
--> $DIR/warn-when-cycle-is-error-in-coherence.rs:13:1
3+
|
4+
LL | #[derive(PartialEq, Default)]
5+
| --------- the second impl is here
6+
...
7+
LL | impl<T, Q> PartialEq<Q> for Interval<T>
8+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the first impl is here
9+
...
10+
LL | Q: ?Sized + PartialOrd,
11+
| ---------- `Interval<_>: PartialOrd` may be considered to hold in future releases, causing the impls to overlap
12+
|
13+
= warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
14+
= note: for more information, see issue #114040 <https://github.com/rust-lang/rust/issues/114040>
15+
= note: impls that are not considered to overlap may be considered to overlap in the future
16+
note: the lint level is defined here
17+
--> $DIR/warn-when-cycle-is-error-in-coherence.rs:1:9
18+
|
19+
LL | #![deny(coinductive_overlap_in_coherence)]
20+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
21+
22+
error: aborting due to previous error
23+

0 commit comments

Comments
 (0)