Rework upcasting

This commit is contained in:
Michael Goulet 2023-07-26 23:54:55 +00:00
parent fcf3006e01
commit 1bb6ae5874
12 changed files with 297 additions and 104 deletions

View File

@ -481,3 +481,31 @@ fn to_trace(
TypeTrace { cause: cause.clone(), values: Sigs(ExpectedFound::new(a_is_expected, a, b)) }
}
}
impl<'tcx> ToTrace<'tcx> for ty::PolyExistentialTraitRef<'tcx> {
fn to_trace(
cause: &ObligationCause<'tcx>,
a_is_expected: bool,
a: Self,
b: Self,
) -> TypeTrace<'tcx> {
TypeTrace {
cause: cause.clone(),
values: ExistentialTraitRef(ExpectedFound::new(a_is_expected, a, b)),
}
}
}
impl<'tcx> ToTrace<'tcx> for ty::PolyExistentialProjection<'tcx> {
fn to_trace(
cause: &ObligationCause<'tcx>,
a_is_expected: bool,
a: Self,
b: Self,
) -> TypeTrace<'tcx> {
TypeTrace {
cause: cause.clone(),
values: ExistentialProjection(ExpectedFound::new(a_is_expected, a, b)),
}
}
}

View File

@ -1635,6 +1635,12 @@ enum Mismatch<'a> {
(false, Mismatch::Fixed(self.tcx.def_descr(expected.def_id)))
}
ValuePairs::Regions(_) => (false, Mismatch::Fixed("lifetime")),
ValuePairs::ExistentialTraitRef(_) => {
(false, Mismatch::Fixed("existential trait ref"))
}
ValuePairs::ExistentialProjection(_) => {
(false, Mismatch::Fixed("existential projection"))
}
};
let Some(vals) = self.values_str(values) else {
// Derived error. Cancel the emitter.
@ -2139,6 +2145,8 @@ fn values_str(
infer::Regions(exp_found) => self.expected_found_str(exp_found),
infer::Terms(exp_found) => self.expected_found_str_term(exp_found),
infer::Aliases(exp_found) => self.expected_found_str(exp_found),
infer::ExistentialTraitRef(exp_found) => self.expected_found_str(exp_found),
infer::ExistentialProjection(exp_found) => self.expected_found_str(exp_found),
infer::TraitRefs(exp_found) => {
let pretty_exp_found = ty::error::ExpectedFound {
expected: exp_found.expected.print_only_trait_path(),

View File

@ -374,6 +374,8 @@ pub enum ValuePairs<'tcx> {
TraitRefs(ExpectedFound<ty::TraitRef<'tcx>>),
PolyTraitRefs(ExpectedFound<ty::PolyTraitRef<'tcx>>),
Sigs(ExpectedFound<ty::FnSig<'tcx>>),
ExistentialTraitRef(ExpectedFound<ty::PolyExistentialTraitRef<'tcx>>),
ExistentialProjection(ExpectedFound<ty::PolyExistentialProjection<'tcx>>),
}
impl<'tcx> ValuePairs<'tcx> {

View File

@ -73,12 +73,15 @@ pub struct GoalCandidate<'tcx> {
pub enum CandidateKind<'tcx> {
/// Probe entered when normalizing the self ty during candidate assembly
NormalizedSelfTyAssembly,
DynUpcastingAssembly,
/// A normal candidate for proving a goal
Candidate {
name: String,
result: QueryResult<'tcx>,
},
Candidate { name: String, result: QueryResult<'tcx> },
/// Used in the probe that wraps normalizing the non-self type for the unsize
/// trait, which is also structurally matched on.
UnsizeAssembly,
/// During upcasting from some source object to target object type, used to
/// do a probe to find out what projection type(s) may be used to prove that
/// the source type upholds all of the target type's object bounds.
UpcastProbe,
}
impl Debug for GoalCandidate<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {

View File

@ -100,8 +100,11 @@ pub(super) fn format_candidate(&mut self, candidate: &GoalCandidate<'_>) -> std:
CandidateKind::NormalizedSelfTyAssembly => {
writeln!(self.f, "NORMALIZING SELF TY FOR ASSEMBLY:")
}
CandidateKind::DynUpcastingAssembly => {
writeln!(self.f, "ASSEMBLING CANDIDATES FOR DYN UPCASTING:")
CandidateKind::UnsizeAssembly => {
writeln!(self.f, "ASSEMBLING CANDIDATES FOR UNSIZING:")
}
CandidateKind::UpcastProbe => {
writeln!(self.f, "PROBING FOR PROJECTION COMPATIBILITY FOR UPCASTING:")
}
CandidateKind::Candidate { name, result } => {
writeln!(self.f, "CANDIDATE {name}: {result:?}")

View File

@ -2734,8 +2734,9 @@ pub struct PrintClosureAsImpl<'tcx> {
// HACK(eddyb) these are exhaustive instead of generic,
// because `for<'tcx>` isn't possible yet.
ty::PolyExistentialPredicate<'tcx>,
ty::PolyExistentialProjection<'tcx>,
ty::PolyExistentialTraitRef<'tcx>,
ty::Binder<'tcx, ty::TraitRef<'tcx>>,
ty::Binder<'tcx, ty::ExistentialTraitRef<'tcx>>,
ty::Binder<'tcx, TraitRefPrintOnlyTraitPath<'tcx>>,
ty::Binder<'tcx, TraitRefPrintOnlyTraitName<'tcx>>,
ty::Binder<'tcx, ty::FnSig<'tcx>>,

View File

@ -444,7 +444,7 @@ fn consider_builtin_unsize_candidates(
Err(NoSolution) => vec![],
};
ecx.probe(|_| CandidateKind::DynUpcastingAssembly).enter(|ecx| {
ecx.probe(|_| CandidateKind::UnsizeAssembly).enter(|ecx| {
let a_ty = goal.predicate.self_ty();
// We need to normalize the b_ty since it's matched structurally
// in the other functions below.
@ -526,7 +526,7 @@ fn consider_builtin_dyn_upcast_candidates(
b_region: ty::Region<'tcx>,
) -> Vec<(CanonicalResponse<'tcx>, BuiltinImplSource)> {
let tcx = self.tcx();
let Goal { predicate: (a_ty, b_ty), .. } = goal;
let Goal { predicate: (a_ty, _b_ty), .. } = goal;
// All of a's auto traits need to be in b's auto traits.
let auto_traits_compatible =
@ -535,51 +535,30 @@ fn consider_builtin_dyn_upcast_candidates(
return vec![];
}
// Try to match `a_ty` against `b_ty`, replacing `a_ty`'s principal trait ref with
// the supertrait principal and subtyping the types.
let unsize_dyn_to_principal =
|ecx: &mut Self, principal: Option<ty::PolyExistentialTraitRef<'tcx>>| {
ecx.probe_candidate("upcast dyn to principle").enter(
|ecx| -> Result<_, NoSolution> {
// Require that all of the trait predicates from A match B, except for
// the auto traits. We do this by constructing a new A type with B's
// auto traits, and equating these types.
let new_a_data = principal
.into_iter()
.map(|trait_ref| trait_ref.map_bound(ty::ExistentialPredicate::Trait))
.chain(a_data.iter().filter(|a| {
matches!(a.skip_binder(), ty::ExistentialPredicate::Projection(_))
}))
.chain(
b_data
.auto_traits()
.map(ty::ExistentialPredicate::AutoTrait)
.map(ty::Binder::dummy),
);
let new_a_data = tcx.mk_poly_existential_predicates_from_iter(new_a_data);
let new_a_ty = Ty::new_dynamic(tcx, new_a_data, b_region, ty::Dyn);
// We also require that A's lifetime outlives B's lifetime.
ecx.eq(goal.param_env, new_a_ty, b_ty)?;
ecx.add_goal(goal.with(tcx, ty::OutlivesPredicate(a_region, b_region)));
ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
},
)
};
let mut responses = vec![];
// If the principal def ids match (or are both none), then we're not doing
// trait upcasting. We're just removing auto traits (or shortening the lifetime).
if a_data.principal_def_id() == b_data.principal_def_id() {
if let Ok(resp) = unsize_dyn_to_principal(self, a_data.principal()) {
if let Ok(resp) = self.consider_builtin_upcast_to_principal(
goal,
a_data,
a_region,
b_data,
b_region,
a_data.principal(),
) {
responses.push((resp, BuiltinImplSource::Misc));
}
} else if let Some(a_principal) = a_data.principal() {
self.walk_vtable(
a_principal.with_self_ty(tcx, a_ty),
|ecx, new_a_principal, _, vtable_vptr_slot| {
if let Ok(resp) = unsize_dyn_to_principal(
ecx,
if let Ok(resp) = ecx.consider_builtin_upcast_to_principal(
goal,
a_data,
a_region,
b_data,
b_region,
Some(new_a_principal.map_bound(|trait_ref| {
ty::ExistentialTraitRef::erase_self_ty(tcx, trait_ref)
})),
@ -631,6 +610,78 @@ fn consider_builtin_unsize_to_dyn(
self.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
}
fn consider_builtin_upcast_to_principal(
&mut self,
goal: Goal<'tcx, (Ty<'tcx>, Ty<'tcx>)>,
a_data: &'tcx ty::List<ty::PolyExistentialPredicate<'tcx>>,
a_region: ty::Region<'tcx>,
b_data: &'tcx ty::List<ty::PolyExistentialPredicate<'tcx>>,
b_region: ty::Region<'tcx>,
upcast_principal: Option<ty::PolyExistentialTraitRef<'tcx>>,
) -> QueryResult<'tcx> {
let param_env = goal.param_env;
// More than one projection in a_ty's bounds may match the projection
// in b_ty's bound. Use this to first determine *which* apply without
// having any inference side-effects. We process obligations because
// unification may initially succeed due to deferred projection equality.
let projection_may_match = |ecx: &mut Self, source_projection, target_projection| {
ecx.probe(|_| CandidateKind::UpcastProbe)
.enter(|ecx| -> Result<(), NoSolution> {
ecx.eq(param_env, source_projection, target_projection)?;
let _ = ecx.try_evaluate_added_goals()?;
Ok(())
})
.is_ok()
};
for bound in b_data {
match bound.skip_binder() {
// Check that a's supertrait (upcast_principal) is compatible
// with the target (b_ty).
ty::ExistentialPredicate::Trait(target_principal) => {
self.eq(param_env, upcast_principal.unwrap(), bound.rebind(target_principal))?;
}
// Check that b_ty's projection is satisfied by exactly one of
// a_ty's projections. First, we look through the list to see if
// any match. If not, error. Then, if *more* than one matches, we
// return ambiguity. Otherwise, if exactly one matches, equate
// it with b_ty's projection.
ty::ExistentialPredicate::Projection(target_projection) => {
let target_projection = bound.rebind(target_projection);
let mut matching_projections =
a_data.projection_bounds().filter(|source_projection| {
projection_may_match(self, *source_projection, target_projection)
});
let Some(source_projection) = matching_projections.next() else {
return Err(NoSolution);
};
if matching_projections.next().is_some() {
return self.evaluate_added_goals_and_make_canonical_response(
Certainty::AMBIGUOUS,
);
}
self.eq(param_env, source_projection, target_projection)?;
}
// Check that b_ty's auto traits are present in a_ty's bounds.
ty::ExistentialPredicate::AutoTrait(def_id) => {
if !a_data.auto_traits().any(|source_def_id| source_def_id == def_id) {
return Err(NoSolution);
}
}
}
}
// Also require that a_ty's lifetime outlives b_ty's lifetime.
self.add_goal(Goal::new(
self.tcx(),
param_env,
ty::Binder::dummy(ty::OutlivesPredicate(a_region, b_region)),
));
self.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
}
/// We have the following builtin impls for arrays:
/// ```ignore (builtin impl example)
/// impl<T: ?Sized, const N: usize> Unsize<[T]> for [T; N] {}

View File

@ -879,68 +879,89 @@ fn confirm_trait_upcasting_unsize_candidate(
// `assemble_candidates_for_unsizing` should ensure there are no late-bound
// regions here. See the comment there for more details.
let source = self.infcx.shallow_resolve(obligation.self_ty().no_bound_vars().unwrap());
let target = obligation.predicate.skip_binder().trait_ref.args.type_at(1);
let target = self.infcx.shallow_resolve(target);
let predicate = obligation.predicate.no_bound_vars().unwrap();
let a_ty = self.infcx.shallow_resolve(predicate.self_ty());
let b_ty = self.infcx.shallow_resolve(predicate.trait_ref.args.type_at(1));
debug!(?source, ?target, "confirm_trait_upcasting_unsize_candidate");
let ty::Dynamic(a_data, a_region, ty::Dyn) = *a_ty.kind() else { bug!() };
let ty::Dynamic(b_data, b_region, ty::Dyn) = *b_ty.kind() else { bug!() };
let source_principal = a_data.principal().unwrap().with_self_ty(tcx, a_ty);
let upcast_principal = util::supertraits(tcx, source_principal).nth(idx).unwrap();
let mut nested = vec![];
let source_trait_ref;
let upcast_trait_ref;
match (source.kind(), target.kind()) {
// TraitA+Kx+'a -> TraitB+Ky+'b (trait upcasting coercion).
(
&ty::Dynamic(ref data_a, r_a, repr_a @ ty::Dyn),
&ty::Dynamic(ref data_b, r_b, ty::Dyn),
) => {
// See `assemble_candidates_for_unsizing` for more info.
// We already checked the compatibility of auto traits within `assemble_candidates_for_unsizing`.
let principal_a = data_a.principal().unwrap();
source_trait_ref = principal_a.with_self_ty(tcx, source);
upcast_trait_ref = util::supertraits(tcx, source_trait_ref).nth(idx).unwrap();
assert_eq!(data_b.principal_def_id(), Some(upcast_trait_ref.def_id()));
let existential_predicate = upcast_trait_ref.map_bound(|trait_ref| {
ty::ExistentialPredicate::Trait(ty::ExistentialTraitRef::erase_self_ty(
tcx, trait_ref,
))
});
let iter = Some(existential_predicate)
.into_iter()
.chain(
data_a
.projection_bounds()
.map(|b| b.map_bound(ty::ExistentialPredicate::Projection)),
)
.chain(
data_b
.auto_traits()
.map(ty::ExistentialPredicate::AutoTrait)
.map(ty::Binder::dummy),
);
let existential_predicates = tcx.mk_poly_existential_predicates_from_iter(iter);
let source_trait = Ty::new_dynamic(tcx, existential_predicates, r_b, repr_a);
// Require that the traits involved in this upcast are **equal**;
// only the **lifetime bound** is changed.
let InferOk { obligations, .. } = self
.infcx
for bound in b_data {
match bound.skip_binder() {
// Check that a's supertrait (upcast_principal) is compatible
// with the target (b_ty).
ty::ExistentialPredicate::Trait(target_principal) => {
nested.extend(
self.infcx
.at(&obligation.cause, obligation.param_env)
.sup(DefineOpaqueTypes::No, target, source_trait)
.map_err(|_| Unimplemented)?;
nested.extend(obligations);
.sup(
DefineOpaqueTypes::No,
upcast_principal.map_bound(|trait_ref| {
ty::ExistentialTraitRef::erase_self_ty(tcx, trait_ref)
}),
bound.rebind(target_principal),
)
.map_err(|_| SelectionError::Unimplemented)?
.into_obligations(),
);
}
// Check that b_ty's projection is satisfied by exactly one of
// a_ty's projections. First, we look through the list to see if
// any match. If not, error. Then, if *more* than one matches, we
// return ambiguity. Otherwise, if exactly one matches, equate
// it with b_ty's projection.
ty::ExistentialPredicate::Projection(target_projection) => {
let target_projection = bound.rebind(target_projection);
let mut matching_projections =
a_data.projection_bounds().filter(|source_projection| {
// Eager normalization means that we can just use can_eq
// here instead of equating and processing obligations.
self.infcx.can_eq(
obligation.param_env,
*source_projection,
target_projection,
)
});
let Some(source_projection) = matching_projections.next() else {
return Err(SelectionError::Unimplemented);
};
if matching_projections.next().is_some() {
// This is incomplete but I don't care. We should never
// have more than one projection that ever applies with
// eager norm and actually implementable traits, since
// you can't have two supertraits like:
// `trait A: B<i32, Assoc = First> + B<i32, Assoc = Second>`
return Err(SelectionError::Unimplemented);
}
nested.extend(
self.infcx
.at(&obligation.cause, obligation.param_env)
.sup(DefineOpaqueTypes::No, source_projection, target_projection)
.map_err(|_| SelectionError::Unimplemented)?
.into_obligations(),
);
}
// Check that b_ty's auto trait is present in a_ty's bounds.
ty::ExistentialPredicate::AutoTrait(def_id) => {
if !a_data.auto_traits().any(|source_def_id| source_def_id == def_id) {
return Err(SelectionError::Unimplemented);
}
}
}
}
let outlives = ty::OutlivesPredicate(r_a, r_b);
// Also require that a_ty's lifetime outlives b_ty's lifetime.
nested.push(Obligation::with_depth(
tcx,
obligation.cause.clone(),
obligation.recursion_depth + 1,
obligation.param_env,
obligation.predicate.rebind(outlives),
ty::Binder::dummy(ty::OutlivesPredicate(a_region, b_region)),
));
}
_ => bug!(),
};
let vtable_segment_callback = {
let mut vptr_offset = 0;
@ -951,7 +972,7 @@ fn confirm_trait_upcasting_unsize_candidate(
}
VtblSegment::TraitOwnEntries { trait_ref, emit_vptr } => {
vptr_offset += count_own_vtable_entries(tcx, trait_ref);
if trait_ref == upcast_trait_ref {
if trait_ref == upcast_principal {
if emit_vptr {
return ControlFlow::Break(Some(vptr_offset));
} else {
@ -969,7 +990,7 @@ fn confirm_trait_upcasting_unsize_candidate(
};
let vtable_vptr_slot =
prepare_vtable_segments(tcx, source_trait_ref, vtable_segment_callback).unwrap();
prepare_vtable_segments(tcx, source_principal, vtable_segment_callback).unwrap();
Ok(ImplSource::Builtin(BuiltinImplSource::TraitUpcasting { vtable_vptr_slot }, nested))
}

View File

@ -0,0 +1,25 @@
// check-pass
// issue: 114035
// revisions: current next
//[next] compile-flags: -Ztrait-solver=next
#![feature(trait_upcasting)]
trait A: B {
type Assoc;
}
trait B {}
fn upcast(a: &dyn A<Assoc = i32>) -> &dyn B {
a
}
// Make sure that we can drop the existential projection `A::Assoc = i32`
// when upcasting `dyn A<Assoc = i32>` to `dyn B`. Before, we used some
// complicated algorithm which required rebuilding a new object type with
// different bounds in order to test that an upcast was valid, but this
// didn't allow upcasting to t that have fewer associated types
// than the source type.
fn main() {}

View File

@ -0,0 +1,14 @@
error[E0308]: mismatched types
--> $DIR/illegal-upcast-from-impl.rs:16:66
|
LL | fn illegal(x: &dyn Sub<Assoc = ()>) -> &dyn Super<Assoc = i32> { x }
| ----------------------- ^ expected trait `Super`, found trait `Sub`
| |
| expected `&dyn Super<Assoc = i32>` because of return type
|
= note: expected reference `&dyn Super<Assoc = i32>`
found reference `&dyn Sub<Assoc = ()>`
error: aborting due to previous error
For more information about this error, try `rustc --explain E0308`.

View File

@ -0,0 +1,14 @@
error[E0308]: mismatched types
--> $DIR/illegal-upcast-from-impl.rs:16:66
|
LL | fn illegal(x: &dyn Sub<Assoc = ()>) -> &dyn Super<Assoc = i32> { x }
| ----------------------- ^ expected trait `Super`, found trait `Sub`
| |
| expected `&dyn Super<Assoc = i32>` because of return type
|
= note: expected reference `&dyn Super<Assoc = i32>`
found reference `&dyn Sub<Assoc = ()>`
error: aborting due to previous error
For more information about this error, try `rustc --explain E0308`.

View File

@ -0,0 +1,23 @@
// revisions: current next
//[next] compile-flags: -Ztrait-solver=next
#![feature(trait_upcasting)]
trait Super {
type Assoc;
}
trait Sub: Super {}
impl<T: ?Sized> Super for T {
type Assoc = i32;
}
fn illegal(x: &dyn Sub<Assoc = ()>) -> &dyn Super<Assoc = i32> { x }
//~^ ERROR mismatched types
// Want to make sure that we can't "upcast" to a supertrait that has a different
// associated type that is instead provided by a blanket impl (and doesn't come
// from the object bounds).
fn main() {}