Skip to content

Commit 56f5704

Browse files
Implement lint against coinductive impl overlap
1 parent 2ae4bed commit 56f5704

File tree

5 files changed

+183
-6
lines changed

5 files changed

+183
-6
lines changed

compiler/rustc_lint_defs/src/builtin.rs

+51
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,56 @@ 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+
/// use std::borrow::Borrow;
4435+
/// use std::cmp::Ordering;
4436+
/// use std::marker::PhantomData;
4437+
///
4438+
/// #[derive(PartialEq, Default)]
4439+
/// pub(crate) struct Interval<T>(T);
4440+
///
4441+
/// impl<T, Q> PartialEq<Q> for Interval<T>
4442+
/// where
4443+
/// Q: PartialOrd,
4444+
/// {
4445+
/// fn eq(&self, other: &Q) -> bool {
4446+
/// todo!()
4447+
/// }
4448+
/// }
4449+
///
4450+
/// impl<T, Q> PartialOrd<Q> for Interval<T>
4451+
/// where
4452+
/// Q: PartialOrd,
4453+
/// {
4454+
/// fn partial_cmp(&self, other: &Q) -> Option<Ordering> {
4455+
/// todo!()
4456+
/// }
4457+
/// }
4458+
/// ```
4459+
///
4460+
/// {{produces}}
4461+
///
4462+
/// ### Explanation
4463+
///
4464+
/// The manual impl of `PartialEq` impl overlaps with the `derive`, since
4465+
/// if we replace `Q = Interval<T>`, then the second impl leads to a cycle:
4466+
/// `PartialOrd for Interval<T> where Interval<T>: Partial`.
4467+
pub COINDUCTIVE_OVERLAP_IN_COHERENCE,
4468+
Warn,
4469+
"impls that are not considered to overlap may be considered to \
4470+
overlap in the future",
4471+
@future_incompatible = FutureIncompatibleInfo {
4472+
reference: "issue #114040 <https://github.com/rust-lang/rust/issues/114040>",
4473+
};
4474+
}
4475+
44254476
declare_lint! {
44264477
/// The `unknown_diagnostic_attributes` lint detects unrecognized diagnostic attributes.
44274478
///

compiler/rustc_trait_selection/src/traits/coherence.rs

+42-3
Original file line numberDiff line numberDiff line change
@@ -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;
@@ -295,9 +296,8 @@ fn impl_intersection_has_impossible_obligation<'cx, 'tcx>(
295296
if infcx.next_trait_solver() {
296297
infcx.evaluate_obligation(obligation).map_or(false, |result| !result.may_apply())
297298
} else {
298-
// We use `evaluate_root_obligation` to correctly track
299-
// intercrate ambiguity clauses. We do not need this in the
300-
// new solver.
299+
// We use `evaluate_root_obligation` to correctly track intercrate
300+
// ambiguity clauses. We cannot use this in the new solver.
301301
selcx.evaluate_root_obligation(obligation).map_or(
302302
false, // Overflow has occurred, and treat the obligation as possibly holding.
303303
|result| !result.may_apply(),
@@ -315,6 +315,45 @@ fn impl_intersection_has_impossible_obligation<'cx, 'tcx>(
315315
.find(obligation_guaranteed_to_fail);
316316

317317
if let Some(failing_obligation) = opt_failing_obligation {
318+
// Check the failing obligation once again, treating inductive cycles as
319+
// ambiguous instead of error.
320+
if !infcx.next_trait_solver()
321+
&& SelectionContext::with_treat_inductive_cycle_as_ambiguous(infcx)
322+
.evaluate_root_obligation(&failing_obligation)
323+
.map_or(true, |result| result.may_apply())
324+
{
325+
let first_local_impl = impl1_header
326+
.impl_def_id
327+
.as_local()
328+
.or(impl2_header.impl_def_id.as_local())
329+
.expect("expected one of the impls to be local");
330+
infcx.tcx.struct_span_lint_hir(
331+
COINDUCTIVE_OVERLAP_IN_COHERENCE,
332+
infcx.tcx.local_def_id_to_hir_id(first_local_impl),
333+
infcx.tcx.def_span(first_local_impl),
334+
"impls that are not considered to overlap may be considered to \
335+
overlap in the future",
336+
|lint| {
337+
lint.span_label(
338+
infcx.tcx.def_span(impl1_header.impl_def_id),
339+
"the first impl is here",
340+
)
341+
.span_label(
342+
infcx.tcx.def_span(impl2_header.impl_def_id),
343+
"the second impl is here",
344+
);
345+
if !failing_obligation.cause.span.is_dummy() {
346+
lint.span_label(
347+
failing_obligation.cause.span,
348+
"this where-clause causes a cycle, but it may be treated \
349+
as possibly holding in the future, causing the impls to overlap",
350+
);
351+
}
352+
lint
353+
},
354+
);
355+
}
356+
318357
debug!("overlap: obligation unsatisfiable {:?}", failing_obligation);
319358
true
320359
} else {

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

+33-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,13 +200,32 @@ enum BuiltinImplConditions<'tcx> {
198200
Ambiguous,
199201
}
200202

203+
enum TreatInductiveCycleAs {
204+
Recur,
205+
Ambig,
206+
}
207+
201208
impl<'cx, 'tcx> SelectionContext<'cx, 'tcx> {
202209
pub fn new(infcx: &'cx InferCtxt<'tcx>) -> SelectionContext<'cx, 'tcx> {
203210
SelectionContext {
204211
infcx,
205212
freshener: infcx.freshener(),
206213
intercrate_ambiguity_causes: None,
207214
query_mode: TraitQueryMode::Standard,
215+
treat_inductive_cycle: TreatInductiveCycleAs::Recur,
216+
}
217+
}
218+
219+
pub fn with_treat_inductive_cycle_as_ambiguous(
220+
infcx: &'cx InferCtxt<'tcx>,
221+
) -> SelectionContext<'cx, 'tcx> {
222+
assert!(infcx.intercrate, "this doesn't do caching yet, so don't use it out of intercrate");
223+
SelectionContext {
224+
infcx,
225+
freshener: infcx.freshener(),
226+
intercrate_ambiguity_causes: None,
227+
query_mode: TraitQueryMode::Standard,
228+
treat_inductive_cycle: TreatInductiveCycleAs::Ambig,
208229
}
209230
}
210231

@@ -719,7 +740,10 @@ impl<'cx, 'tcx> SelectionContext<'cx, 'tcx> {
719740
stack.update_reached_depth(stack_arg.1);
720741
return Ok(EvaluatedToOk);
721742
} else {
722-
return Ok(EvaluatedToRecur);
743+
match self.treat_inductive_cycle {
744+
TreatInductiveCycleAs::Ambig => return Ok(EvaluatedToAmbig),
745+
TreatInductiveCycleAs::Recur => return Ok(EvaluatedToRecur),
746+
}
723747
}
724748
}
725749
return Ok(EvaluatedToOk);
@@ -837,7 +861,10 @@ impl<'cx, 'tcx> SelectionContext<'cx, 'tcx> {
837861
}
838862
}
839863
ProjectAndUnifyResult::FailedNormalization => Ok(EvaluatedToAmbig),
840-
ProjectAndUnifyResult::Recursive => Ok(EvaluatedToRecur),
864+
ProjectAndUnifyResult::Recursive => match self.treat_inductive_cycle {
865+
TreatInductiveCycleAs::Ambig => return Ok(EvaluatedToAmbig),
866+
TreatInductiveCycleAs::Recur => return Ok(EvaluatedToRecur),
867+
},
841868
ProjectAndUnifyResult::MismatchedProjectionTypes(_) => Ok(EvaluatedToErr),
842869
}
843870
}
@@ -1151,7 +1178,10 @@ impl<'cx, 'tcx> SelectionContext<'cx, 'tcx> {
11511178
Some(EvaluatedToOk)
11521179
} else {
11531180
debug!("evaluate_stack --> recursive, inductive");
1154-
Some(EvaluatedToRecur)
1181+
match self.treat_inductive_cycle {
1182+
TreatInductiveCycleAs::Ambig => Some(EvaluatedToAmbig),
1183+
TreatInductiveCycleAs::Recur => Some(EvaluatedToRecur),
1184+
}
11551185
}
11561186
} else {
11571187
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 an inductive cycle right now.
13+
impl<T, Q> PartialEq<Q> for Interval<T>
14+
//~^ ERROR impls that are not considered to overlap may be considered to overlap 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,22 @@
1+
error: impls that are not considered to overlap may be considered to overlap 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+
| ---------- this where-clause causes a cycle, but it may be treated as possibly holding in the future, 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: the lint level is defined here
16+
--> $DIR/warn-when-cycle-is-error-in-coherence.rs:1:9
17+
|
18+
LL | #![deny(coinductive_overlap_in_coherence)]
19+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
20+
21+
error: aborting due to previous error
22+

0 commit comments

Comments
 (0)