Skip to content

Commit 88fd1c2

Browse files
committed
Auto merge of #792 - lowr:fix/generalize-alias-alias-eq-clause, r=jackh726
Generalize program clause for `AliasEq` goal with nested alias Consider a goal: `AliasEq(<<?0 as X>::A as Y>::B = <<?1 as X>::A as Y>::B)`. This is expected to (eventually) flounder when the traits are non-enumerable and the variables are yet to be known, but it's currently **disproven**: 1. the goal is canonicalized as follows: - `forall<T, U> AliasEq(<<T as X>::A as Y>::B = <<U as X>::A as Y>::B)` 1. we produce the following `ProgramClause` that could match: - `forall<T, U, ..> AliasEq(<<T as X>::A as Y>::B = <<U as X>::A as Y>::B) :- AliasEq(..), AliasEq(..)` 1. the consequence of the (instantiated) clause is unified with the goal, which produces a subgoal: - `AliasEq(<<?0 as X>::A as Y>::B = <<?1 as X>::A as Y>::B)` - this is because when we unify rhs of `AliasEq`, we see two `AliasTy`s that we cannot unify structurally, forcing us to produce the subgoal 1. boom, cycle emerged, goal disproven! The problem is that because we're reusing the original goal as the consequence of the program clause we expect them to unify structurally, however we cannot unify aliases structurally in general (e.g. `<?0 as X>::A = <?1 as X>::A` does not imply `?0 = ?1`). This PR solves it by placing a fresh variable in rhs of the consequence `AliasEq` instead of reusing the original term. This ensures that rhs of the original `AliasEq` goal is preserved via unification without producing subgoals even if it's an alias. See rust-lang/rust-analyzer#14369 for a real world case where this issue arises.
2 parents 1635ed5 + 91c24e3 commit 88fd1c2

File tree

2 files changed

+98
-43
lines changed

2 files changed

+98
-43
lines changed

chalk-solve/src/clauses.rs

Lines changed: 61 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -512,12 +512,7 @@ pub fn program_clauses_that_could_match<I: Interner>(
512512
// An alias could normalize to anything, including an
513513
// opaque type, so push a clause that asks for the self
514514
// type to be normalized and return.
515-
push_alias_alias_eq_clause(
516-
builder,
517-
proj.clone(),
518-
alias_eq.ty.clone(),
519-
alias.clone(),
520-
);
515+
push_alias_alias_eq_clause(builder, proj.clone(), alias.clone());
521516
return Ok(clauses);
522517
}
523518
TyKind::OpaqueType(opaque_ty_id, _) => {
@@ -781,12 +776,10 @@ fn push_alias_implemented_clause<I: Interner>(
781776
// TODO: instead generate clauses without reference to the specific type parameters of the goal?
782777
let generalized = generalize::Generalize::apply(interner, (trait_ref, alias));
783778
builder.push_binders(generalized, |builder, (trait_ref, alias)| {
784-
let binders = Binders::with_fresh_type_var(interner, |ty_var| ty_var);
785-
786779
// forall<..., T> {
787780
// <X as Y>::Z: Trait :- T: Trait, <X as Y>::Z == T
788781
// }
789-
builder.push_binders(binders, |builder, bound_var| {
782+
builder.push_bound_ty(|builder, bound_var| {
790783
let fresh_self_subst = Substitution::from_iter(
791784
interner,
792785
std::iter::once(bound_var.clone().cast(interner)).chain(
@@ -816,7 +809,6 @@ fn push_alias_implemented_clause<I: Interner>(
816809
fn push_alias_alias_eq_clause<I: Interner>(
817810
builder: &mut ClauseBuilder<'_, I>,
818811
projection_ty: ProjectionTy<I>,
819-
ty: Ty<I>,
820812
alias: AliasTy<I>,
821813
) {
822814
let interner = builder.interner();
@@ -827,43 +819,69 @@ fn push_alias_alias_eq_clause<I: Interner>(
827819
assert_eq!(*self_ty.kind(interner), TyKind::Alias(alias.clone()));
828820

829821
// TODO: instead generate clauses without reference to the specific type parameters of the goal?
830-
let generalized = generalize::Generalize::apply(interner, (projection_ty, ty, alias));
831-
builder.push_binders(generalized, |builder, (projection_ty, ty, alias)| {
832-
let binders = Binders::with_fresh_type_var(interner, |ty_var| ty_var);
833-
834-
// forall<..., T> {
822+
let generalized = generalize::Generalize::apply(interner, (projection_ty, alias));
823+
builder.push_binders(generalized, |builder, (projection_ty, alias)| {
824+
// Given the following canonical goal:
825+
//
826+
// ```
827+
// forall<...> {
828+
// <<X as Y>::A as Z>::B == W
829+
// }
830+
// ```
831+
//
832+
// we generate:
833+
//
834+
// ```
835+
// forall<..., T, U> {
835836
// <<X as Y>::A as Z>::B == U :- <T as Z>::B == U, <X as Y>::A == T
836837
// }
837-
builder.push_binders(binders, |builder, bound_var| {
838-
let (_, trait_args, assoc_args) = builder.db.split_projection(&projection_ty);
839-
let fresh_self_subst = Substitution::from_iter(
840-
interner,
841-
assoc_args
842-
.iter()
843-
.cloned()
844-
.chain(std::iter::once(bound_var.clone().cast(interner)))
845-
.chain(trait_args[1..].iter().cloned()),
846-
);
847-
let fresh_alias = AliasTy::Projection(ProjectionTy {
848-
associated_ty_id: projection_ty.associated_ty_id,
849-
substitution: fresh_self_subst,
850-
});
851-
builder.push_clause(
852-
DomainGoal::Holds(WhereClause::AliasEq(AliasEq {
853-
alias: AliasTy::Projection(projection_ty.clone()),
854-
ty: ty.clone(),
855-
})),
856-
&[
857-
DomainGoal::Holds(WhereClause::AliasEq(AliasEq {
858-
alias: fresh_alias,
859-
ty: ty.clone(),
860-
})),
838+
// ```
839+
//
840+
// `T` and `U` are `intermediate_eq_ty` and `eq_ty` respectively below.
841+
//
842+
// Note that we used to "reuse" `W` and push:
843+
//
844+
// ```
845+
// forall<..., T> {
846+
// <<X as Y>::A as Z>::B == W :- <T as Z>::B == W, <X as Y>::A == T
847+
// }
848+
// ```
849+
//
850+
// but it caused a cycle which led to false `NoSolution` under certain conditions, in
851+
// particular when `W` itself is a nested projection type. See test
852+
// `nested_proj_eq_nested_proj_should_flounder`.
853+
builder.push_bound_ty(|builder, intermediate_eq_ty| {
854+
builder.push_bound_ty(|builder, eq_ty| {
855+
let (_, trait_args, assoc_args) = builder.db.split_projection(&projection_ty);
856+
let fresh_self_subst = Substitution::from_iter(
857+
interner,
858+
assoc_args
859+
.iter()
860+
.cloned()
861+
.chain(std::iter::once(intermediate_eq_ty.clone().cast(interner)))
862+
.chain(trait_args[1..].iter().cloned()),
863+
);
864+
let fresh_alias = AliasTy::Projection(ProjectionTy {
865+
associated_ty_id: projection_ty.associated_ty_id,
866+
substitution: fresh_self_subst,
867+
});
868+
builder.push_clause(
861869
DomainGoal::Holds(WhereClause::AliasEq(AliasEq {
862-
alias: alias.clone(),
863-
ty: bound_var,
870+
alias: AliasTy::Projection(projection_ty.clone()),
871+
ty: eq_ty.clone(),
864872
})),
865-
],
866-
);
873+
&[
874+
DomainGoal::Holds(WhereClause::AliasEq(AliasEq {
875+
alias: fresh_alias,
876+
ty: eq_ty,
877+
})),
878+
DomainGoal::Holds(WhereClause::AliasEq(AliasEq {
879+
alias,
880+
ty: intermediate_eq_ty,
881+
})),
882+
],
883+
);
884+
});
867885
});
868886
});
869887
}

tests/test/projection.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1189,3 +1189,40 @@ fn projection_from_super_trait_bounds() {
11891189
}
11901190
}
11911191
}
1192+
1193+
#[test]
1194+
fn nested_proj_eq_nested_proj_should_flounder() {
1195+
test! {
1196+
program {
1197+
#[non_enumerable]
1198+
trait Trait1 {
1199+
type Assoc: Trait2;
1200+
}
1201+
#[non_enumerable]
1202+
trait Trait2 {
1203+
type Assoc;
1204+
}
1205+
1206+
impl Trait1 for () {
1207+
type Assoc = ();
1208+
}
1209+
impl Trait1 for i32 {
1210+
type Assoc = ();
1211+
}
1212+
impl Trait2 for () {
1213+
type Assoc = ();
1214+
}
1215+
}
1216+
1217+
goal {
1218+
exists<T, U> {
1219+
<<T as Trait1>::Assoc as Trait2>::Assoc = <<U as Trait1>::Assoc as Trait2>::Assoc
1220+
}
1221+
} yields[SolverChoice::slg_default()] {
1222+
// FIXME
1223+
expect![[r#"Ambiguous; definite substitution for<?U0> { [?0 := ^0.0, ?1 := ^0.0] }"#]]
1224+
} yields[SolverChoice::recursive_default()] {
1225+
expect![[r#"Ambiguous; no inference guidance"#]]
1226+
}
1227+
}
1228+
}

0 commit comments

Comments
 (0)