Skip to content

Conversation

@ShoyuVanilla
Copy link
Member

Fixes #17866

The root cause of #17866 is quite horrifyng 😨

trait T {
    type A;
}

type Foo = <S as T>::A; // note that S isn't defined

fn main() {
    Foo {}
}

While inferencing alias type Foo = <S as T>::A;

TypeNs::TypeAliasId(it) => {
let resolved_seg = match unresolved {
None => path.segments().last().unwrap(),
Some(n) => path.segments().get(path.segments().len() - n - 1).unwrap(),
};
let substs =
ctx.substs_from_path_segment(resolved_seg, Some(it.into()), true, None);
let ty = self.db.ty(it.into());
let ty = self.insert_type_vars(ty.substitute(Interner, &substs));
self.resolve_variant_on_alias(ty, unresolved, mod_path)

the error type S in it is substituted by inference var in L1396 above as below;

/// Replaces `Ty::Error` by a new type var, so we can maybe still infer it.
pub(super) fn insert_type_vars_shallow(&mut self, ty: Ty) -> Ty {
match ty.kind(Interner) {
TyKind::Error => self.new_type_var(),

This new inference var's index is 1, as the type inferecing procedure here previously inserted another inference var into same InferenceTable.

But after that, the projection type made from the above then passed to the following function;

pub(crate) fn normalize_projection_query(
db: &dyn HirDatabase,
projection: ProjectionTy,
env: Arc<TraitEnvironment>,
) -> Ty {
let mut table = InferenceTable::new(db, env);
let ty = table.normalize_projection_ty(projection);
table.resolve_completely(ty)
}

here, a whole new InferenceTable is made, without any inference var and in the L94, this table calls;

pub(crate) fn normalize_projection_ty(&mut self, proj_ty: ProjectionTy) -> Ty {
let var = self.new_type_var();
let alias_eq = AliasEq { alias: AliasTy::Projection(proj_ty), ty: var.clone() };
let obligation = alias_eq.cast(Interner);
self.register_obligation(obligation);
var
}

And while registering AliasEq obligation, this obligation contains inference var ?1 made from the previous table, but this table has only one inference var ?0 made at L365.
So, the chalk panics when we try to canonicalize that obligation to register it, because the obligation contains an inference var ?1 that the canonicalizing table doesn't have.

Currently, we are calling InferenceTable::new() to do some normalizing, unifying or coercing things to some targets that might contain inference var that the new table doesn't have.
I think that this is quite dangerous footgun because the inference var is just an index that does not contain the information which table does it made from, so sometimes this "foreign" index might cause panic like this case, or point at the wrong variable.

This PR mitigates such behaviour simply by inserting sufficient number of inference vars to new table to avoid such problem.
This strategy doesn't harm current r-a's intention because the inference vars that passed into new tables are just "unresolved" variables in current r-a, so this is just making sure that such "unresolved" variables exist in the new table

@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Aug 13, 2024
@ShoyuVanilla ShoyuVanilla changed the title fix: Panic while canonicalizing errornous projection type fix: Panic while canonicalizing erroneous projection type Aug 13, 2024
@davidbarsky
Copy link
Contributor

And while registering AliasEq obligation, this obligation contains inference var ?1 made from the previous table, but this table has only one inference var ?0 made at L365.
I think that this is quite dangerous footgun because the inference var is just an index that does not contain the information which table does it made from, so sometimes this "foreign" index might cause panic like this case, or point at the wrong variable.

I'm trying to remember their names, but there were a few arena crates whose keys tracked its creator's provenance. Do you think an approach like this would help here?

@ShoyuVanilla
Copy link
Member Author

I'm trying to remember their names, but there were a few arena crates whose keys tracked its creator's provenance. Do you think an approach like this would help here?

I think that this PR is just a mitigation and what you said is the "correct" way to fix this, maybe.

@davidbarsky
Copy link
Contributor

Yes, sorry! Lemme merge this. @bors r+

I'll make a new issue.

@bors
Copy link
Contributor

bors commented Aug 13, 2024

📌 Commit 8bcaba1 has been approved by davidbarsky

It is now in the queue for this repository.

bors added a commit that referenced this pull request Aug 13, 2024
fix: Panic while canonicalizing erroneous projection type

Fixes #17866

The root cause of #17866 is quite horrifyng 😨

```rust
trait T {
    type A;
}

type Foo = <S as T>::A; // note that S isn't defined

fn main() {
    Foo {}
}
```

While inferencing alias type `Foo = <S as T>::A`;

https://github.com/rust-lang/rust-analyzer/blob/78c2bdce860dbd996a8083224d01a96660dd6a15/crates/hir-ty/src/infer.rs#L1388-L1398

the error type `S` in it is substituted by inference var in L1396 above as below;

https://github.com/rust-lang/rust-analyzer/blob/78c2bdce860dbd996a8083224d01a96660dd6a15/crates/hir-ty/src/infer/unify.rs#L866-L869

This new inference var's index is `1`, as the type inferecing procedure here previously inserted another inference var into same `InferenceTable`.

But after that, the projection type made from the above then passed to the following function;

https://github.com/rust-lang/rust-analyzer/blob/78c2bdce860dbd996a8083224d01a96660dd6a15/crates/hir-ty/src/traits.rs#L88-L96

here, a whole new `InferenceTable` is made, without any inference var and in the L94, this table calls;

https://github.com/rust-lang/rust-analyzer/blob/78c2bdce860dbd996a8083224d01a96660dd6a15/crates/hir-ty/src/infer/unify.rs#L364-L370

And while registering `AliasEq` `obligation`, this obligation contains inference var `?1` made from the previous table, but this table has only one inference var `?0` made at L365.
So, the chalk panics when we try to canonicalize that obligation to register it, because the obligation contains an inference var `?1` that the canonicalizing table doesn't have.

Currently, we are calling `InferenceTable::new()` to do some normalizing, unifying or coercing things to some targets that might contain inference var that the new table doesn't have.
I think that this is quite dangerous footgun because the inference var is just an index that does not contain the information which table does it made from, so sometimes this "foreign" index might cause panic like this case, or point at the wrong variable.

This PR mitigates such behaviour simply by inserting sufficient number of inference vars to new table to avoid such problem.
This strategy doesn't harm current r-a's intention because the inference vars that passed into new tables are just "unresolved" variables in current r-a, so this is just making sure that such "unresolved" variables exist in the new table
@bors
Copy link
Contributor

bors commented Aug 13, 2024

⌛ Testing commit 8bcaba1 with merge 0bc8501...

@flodiebold
Copy link
Member

flodiebold commented Aug 13, 2024

normalize_projection_query is a Salsa query, so its input should really be canonicalized (i.e. it should take a Canonical<ProjectionTy>). Canonicalization replaces inference vars by bound vars in order of appearance, which has the two effects of 1. "freeing" the type of the inference table it came from, and 2. making types that are equal in structure actually equal (you don't care if you have Option<?2> or Option<?45>), which is important for proper caching of the query. Taking a Canonical also serves as a check that there are no inference variables in the type.

On the other side, apparently this normalize_projection query gets called during inference, which is probably the cause of this bug (the only other place it gets called is in hir). That code should rather be using table.normalize_projection_ty directly.

@flodiebold
Copy link
Member

@bors r-

@davidbarsky
Copy link
Contributor

@bors r-

Sorry about that!

@flodiebold
Copy link
Member

A bit too slow in writing my comment 😅

@ShoyuVanilla
Copy link
Member Author

ShoyuVanilla commented Aug 13, 2024

Okay, I'll rethink about this tommorrow(it's past midnight here 😅)

@ShoyuVanilla ShoyuVanilla marked this pull request as draft August 13, 2024 17:07
@bors
Copy link
Contributor

bors commented Aug 13, 2024

☀️ Try build successful - checks-actions
Build commit: 0bc8501 (0bc85018e9660cd35a81007ed8ca8b58587fab9e)

@flodiebold
Copy link
Member

Currently, we are calling InferenceTable::new() to do some normalizing, unifying or coercing things to some targets that might contain inference var that the new table doesn't have.

Basically, a type that still contains inference vars "crossing over" to a new inference table is always a bug. Most of the places you changed already take a Canonical, so they should be fine; a few are only intended for use by hir or analysis after type-checking. For these a debug assertion that the type doesn't contain inference vars might be good.

@lnicola
Copy link
Member

lnicola commented Aug 13, 2024

there were a few arena crates whose keys tracked its creator's provenance

It no longer matters, but I think they're called generational indexes or arenas.

@ShoyuVanilla ShoyuVanilla marked this pull request as ready for review August 14, 2024 00:39
@ShoyuVanilla
Copy link
Member Author

normalize_projection_query is a Salsa query, so its input should really be canonicalized (i.e. it should take a Canonical). Canonicalization replaces inference vars by bound vars in order of appearance, which has the two effects of 1. "freeing" the type of the inference table it came from, and 2. making types that are equal in structure actually equal (you don't care if you have Option<?2> or Option<?45>), which is important for proper caching of the query. Taking a Canonical also serves as a check that there are no inference variables in the type.

On the other side, apparently this normalize_projection query gets called during inference, which is probably the cause of this bug (the only other place it gets called is in hir). That code should rather be using table.normalize_projection_ty directly.

I was too obssesed with the projection type containing ivs and learned a lot from your comments. Thanks 😄

sanbei101

This comment was marked as spam.

Copy link
Member

@flodiebold flodiebold left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bors delegate+

TyKind::Alias(AliasTy::Projection(proj_ty)) => {
self.db.normalize_projection(proj_ty.clone(), self.table.trait_env.clone())
let ty = self.table.normalize_projection_ty(proj_ty.clone());
self.table.resolve_completely(ty)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolve_completely is the wrong thing to use during inference since it applies fallback. I think this should just be resolve_ty_shallow

Comment on lines 97 to 121
struct InferenceVarVisitor(Option<()>);
impl TypeVisitor<Interner> for InferenceVarVisitor {
type BreakTy = ();

fn as_dyn(&mut self) -> &mut dyn TypeVisitor<Interner, BreakTy = Self::BreakTy> {
self
}

fn interner(&self) -> Interner {
Interner
}

fn visit_inference_var(
&mut self,
_var: chalk_ir::InferenceVar,
_outer_binder: DebruijnIndex,
) -> std::ops::ControlFlow<Self::BreakTy> {
self.0 = Some(());
std::ops::ControlFlow::Break(())
}
}

let mut visitor = InferenceVarVisitor(None);
projection.visit_with(visitor.as_dyn(), DebruijnIndex::INNERMOST);
if visitor.0.is_some() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be a function has_inference_variables. Also, the flags in a Ty already store whether it contains any inference variable, so it could be easier to use that :)

@ShoyuVanilla
Copy link
Member Author

@bors r+

@bors
Copy link
Contributor

bors commented Aug 14, 2024

@ShoyuVanilla: 🔑 Insufficient privileges: Not in reviewers

@lnicola
Copy link
Member

lnicola commented Aug 14, 2024

@bors r+

@bors
Copy link
Contributor

bors commented Aug 14, 2024

📌 Commit e6d8970 has been approved by lnicola

It is now in the queue for this repository.

@bors
Copy link
Contributor

bors commented Aug 14, 2024

⌛ Testing commit e6d8970 with merge 64a1405...

@bors
Copy link
Contributor

bors commented Aug 14, 2024

☀️ Test successful - checks-actions
Approved by: lnicola
Pushing 64a1405 to master...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-review Status: Awaiting review from the assignee but also interested parties.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Chalk panic accessing associated type with fully qualified syntax

7 participants