From 8bd30e9b3fd27884aba24f5d4832fe64dc36276a Mon Sep 17 00:00:00 2001 From: Tavo Annus Date: Sun, 25 Feb 2024 16:50:34 +0200 Subject: [PATCH 1/3] Improve generics handling in term search --- crates/hir/src/lib.rs | 5 + crates/hir/src/term_search.rs | 46 ++++-- crates/hir/src/term_search/expr.rs | 15 ++ crates/hir/src/term_search/tactics.rs | 137 ++++++++++-------- .../ide-assists/src/handlers/term_search.rs | 5 +- crates/ide-completion/src/render.rs | 1 + 6 files changed, 136 insertions(+), 73 deletions(-) diff --git a/crates/hir/src/lib.rs b/crates/hir/src/lib.rs index a0237e3b90..14066dee5f 100644 --- a/crates/hir/src/lib.rs +++ b/crates/hir/src/lib.rs @@ -3856,6 +3856,11 @@ impl Type { Type { env: ty.env, ty: TyBuilder::slice(ty.ty) } } + pub fn new_tuple(krate: CrateId, tys: &[Type]) -> Type { + let tys = tys.iter().map(|it| it.ty.clone()); + Type { env: TraitEnvironment::empty(krate), ty: TyBuilder::tuple_with(tys) } + } + pub fn is_unit(&self) -> bool { matches!(self.ty.kind(Interner), TyKind::Tuple(0, ..)) } diff --git a/crates/hir/src/term_search.rs b/crates/hir/src/term_search.rs index 72762007dc..68244b1272 100644 --- a/crates/hir/src/term_search.rs +++ b/crates/hir/src/term_search.rs @@ -72,6 +72,10 @@ impl AlternativeExprs { AlternativeExprs::Many => (), } } + + fn is_many(&self) -> bool { + matches!(self, AlternativeExprs::Many) + } } /// # Lookup table for term search @@ -103,27 +107,36 @@ struct LookupTable { impl LookupTable { /// Initialize lookup table - fn new(many_threshold: usize) -> Self { + fn new(many_threshold: usize, goal: Type) -> Self { let mut res = Self { many_threshold, ..Default::default() }; res.new_types.insert(NewTypesKey::ImplMethod, Vec::new()); res.new_types.insert(NewTypesKey::StructProjection, Vec::new()); + res.types_wishlist.insert(goal); res } /// Find all `Expr`s that unify with the `ty` - fn find(&self, db: &dyn HirDatabase, ty: &Type) -> Option> { - self.data + fn find(&mut self, db: &dyn HirDatabase, ty: &Type) -> Option> { + let res = self + .data .iter() .find(|(t, _)| t.could_unify_with_deeply(db, ty)) - .map(|(t, tts)| tts.exprs(t)) + .map(|(t, tts)| tts.exprs(t)); + + if res.is_none() { + self.types_wishlist.insert(ty.clone()); + } + + res } /// Same as find but automatically creates shared reference of types in the lookup /// /// For example if we have type `i32` in data and we query for `&i32` it map all the type /// trees we have for `i32` with `Expr::Reference` and returns them. - fn find_autoref(&self, db: &dyn HirDatabase, ty: &Type) -> Option> { - self.data + fn find_autoref(&mut self, db: &dyn HirDatabase, ty: &Type) -> Option> { + let res = self + .data .iter() .find(|(t, _)| t.could_unify_with_deeply(db, ty)) .map(|(t, it)| it.exprs(t)) @@ -139,7 +152,13 @@ impl LookupTable { .map(|expr| Expr::Reference(Box::new(expr))) .collect() }) - }) + }); + + if res.is_none() { + self.types_wishlist.insert(ty.clone()); + } + + res } /// Insert new type trees for type @@ -149,7 +168,12 @@ impl LookupTable { /// but they clearly do not unify themselves. fn insert(&mut self, ty: Type, exprs: impl Iterator) { match self.data.get_mut(&ty) { - Some(it) => it.extend_with_threshold(self.many_threshold, exprs), + Some(it) => { + it.extend_with_threshold(self.many_threshold, exprs); + if it.is_many() { + self.types_wishlist.remove(&ty); + } + } None => { self.data.insert(ty.clone(), AlternativeExprs::new(self.many_threshold, exprs)); for it in self.new_types.values_mut() { @@ -206,8 +230,8 @@ impl LookupTable { } /// Types queried but not found - fn take_types_wishlist(&mut self) -> FxHashSet { - std::mem::take(&mut self.types_wishlist) + fn types_wishlist(&mut self) -> &FxHashSet { + &self.types_wishlist } } @@ -272,7 +296,7 @@ pub fn term_search(ctx: &TermSearchCtx<'_, DB>) -> Vec { defs.insert(def); }); - let mut lookup = LookupTable::new(ctx.config.many_alternatives_threshold); + let mut lookup = LookupTable::new(ctx.config.many_alternatives_threshold, ctx.goal.clone()); // Try trivial tactic first, also populates lookup table let mut solutions: Vec = tactics::trivial(ctx, &defs, &mut lookup).collect(); diff --git a/crates/hir/src/term_search/expr.rs b/crates/hir/src/term_search/expr.rs index 254fbe7e2b..2d0c5630e1 100644 --- a/crates/hir/src/term_search/expr.rs +++ b/crates/hir/src/term_search/expr.rs @@ -138,6 +138,8 @@ pub enum Expr { Variant { variant: Variant, generics: Vec, params: Vec }, /// Struct construction Struct { strukt: Struct, generics: Vec, params: Vec }, + /// Tuple construction + Tuple { ty: Type, params: Vec }, /// Struct field access Field { expr: Box, field: Field }, /// Passing type as reference (with `&`) @@ -366,6 +368,18 @@ impl Expr { let prefix = mod_item_path_str(sema_scope, &ModuleDef::Adt(Adt::Struct(*strukt)))?; Ok(format!("{prefix}{inner}")) } + Expr::Tuple { params, .. } => { + let args = params + .iter() + .map(|a| { + a.gen_source_code(sema_scope, many_formatter, prefer_no_std, prefer_prelude) + }) + .collect::, DisplaySourceCodeError>>()? + .into_iter() + .join(", "); + let res = format!("({args})"); + Ok(res) + } Expr::Field { expr, field } => { if expr.contains_many_in_illegal_pos() { return Ok(many_formatter(&expr.ty(db))); @@ -420,6 +434,7 @@ impl Expr { Expr::Struct { strukt, generics, .. } => { Adt::from(*strukt).ty_with_args(db, generics.iter().cloned()) } + Expr::Tuple { ty, .. } => ty.clone(), Expr::Field { expr, field } => field.ty_with_args(db, expr.ty(db).type_arguments()), Expr::Reference(it) => it.ty(db), Expr::Many(ty) => ty.clone(), diff --git a/crates/hir/src/term_search/tactics.rs b/crates/hir/src/term_search/tactics.rs index edbf75affe..93a780d470 100644 --- a/crates/hir/src/term_search/tactics.rs +++ b/crates/hir/src/term_search/tactics.rs @@ -109,7 +109,6 @@ pub(super) fn type_constructor<'a, DB: HirDatabase>( lookup: &mut LookupTable, parent_enum: Enum, variant: Variant, - goal: &Type, config: &TermSearchConfig, ) -> Vec<(Type, Vec)> { // Ignore unstable @@ -143,11 +142,14 @@ pub(super) fn type_constructor<'a, DB: HirDatabase>( let non_default_type_params_len = type_params.iter().filter(|it| it.default(db).is_none()).count(); + let enum_ty_shallow = Adt::from(parent_enum).ty(db); let generic_params = lookup - .iter_types() - .collect::>() // Force take ownership + .types_wishlist() + .clone() .into_iter() - .permutations(non_default_type_params_len); + .filter(|ty| ty.could_unify_with(db, &enum_ty_shallow)) + .map(|it| it.type_arguments().collect::>()) + .chain((non_default_type_params_len == 0).then_some(Vec::new())); generic_params .filter_map(move |generics| { @@ -155,17 +157,11 @@ pub(super) fn type_constructor<'a, DB: HirDatabase>( let mut g = generics.into_iter(); let generics: Vec<_> = type_params .iter() - .map(|it| it.default(db).unwrap_or_else(|| g.next().expect("No generic"))) - .collect(); + .map(|it| it.default(db).or_else(|| g.next())) + .collect::>()?; let enum_ty = Adt::from(parent_enum).ty_with_args(db, generics.iter().cloned()); - // Allow types with generics only if they take us straight to goal for - // performance reasons - if !generics.is_empty() && !enum_ty.could_unify_with_deeply(db, goal) { - return None; - } - // Ignore types that have something to do with lifetimes if config.enable_borrowcheck && enum_ty.contains_reference(db) { return None; @@ -199,21 +195,37 @@ pub(super) fn type_constructor<'a, DB: HirDatabase>( .filter_map(move |def| match def { ScopeDef::ModuleDef(ModuleDef::Variant(it)) => { let variant_exprs = - variant_helper(db, lookup, it.parent_enum(db), *it, &ctx.goal, &ctx.config); + variant_helper(db, lookup, it.parent_enum(db), *it, &ctx.config); if variant_exprs.is_empty() { return None; } - lookup.mark_fulfilled(ScopeDef::ModuleDef(ModuleDef::Variant(*it))); + if GenericDef::from(it.parent_enum(db)) + .type_or_const_params(db) + .into_iter() + .filter_map(|it| it.as_type_param(db)) + .all(|it| it.default(db).is_some()) + { + lookup.mark_fulfilled(ScopeDef::ModuleDef(ModuleDef::Variant(*it))); + } Some(variant_exprs) } ScopeDef::ModuleDef(ModuleDef::Adt(Adt::Enum(enum_))) => { let exprs: Vec<(Type, Vec)> = enum_ .variants(db) .into_iter() - .flat_map(|it| variant_helper(db, lookup, *enum_, it, &ctx.goal, &ctx.config)) + .flat_map(|it| variant_helper(db, lookup, *enum_, it, &ctx.config)) .collect(); - if !exprs.is_empty() { + if exprs.is_empty() { + return None; + } + + if GenericDef::from(*enum_) + .type_or_const_params(db) + .into_iter() + .filter_map(|it| it.as_type_param(db)) + .all(|it| it.default(db).is_some()) + { lookup.mark_fulfilled(ScopeDef::ModuleDef(ModuleDef::Adt(Adt::Enum(*enum_)))); } @@ -249,11 +261,14 @@ pub(super) fn type_constructor<'a, DB: HirDatabase>( let non_default_type_params_len = type_params.iter().filter(|it| it.default(db).is_none()).count(); + let struct_ty_shallow = Adt::from(*it).ty(db); let generic_params = lookup - .iter_types() - .collect::>() // Force take ownership + .types_wishlist() + .clone() .into_iter() - .permutations(non_default_type_params_len); + .filter(|ty| ty.could_unify_with(db, &struct_ty_shallow)) + .map(|it| it.type_arguments().collect::>()) + .chain((non_default_type_params_len == 0).then_some(Vec::new())); let exprs = generic_params .filter_map(|generics| { @@ -261,22 +276,11 @@ pub(super) fn type_constructor<'a, DB: HirDatabase>( let mut g = generics.into_iter(); let generics: Vec<_> = type_params .iter() - .map(|it| { - it.default(db) - .unwrap_or_else(|| g.next().expect("Missing type param")) - }) - .collect(); + .map(|it| it.default(db).or_else(|| g.next())) + .collect::>()?; let struct_ty = Adt::from(*it).ty_with_args(db, generics.iter().cloned()); - // Allow types with generics only if they take us straight to goal for - // performance reasons - if non_default_type_params_len != 0 - && struct_ty.could_unify_with_deeply(db, &ctx.goal) - { - return None; - } - // Ignore types that have something to do with lifetimes if ctx.config.enable_borrowcheck && struct_ty.contains_reference(db) { return None; @@ -309,8 +313,12 @@ pub(super) fn type_constructor<'a, DB: HirDatabase>( .collect() }; - lookup - .mark_fulfilled(ScopeDef::ModuleDef(ModuleDef::Adt(Adt::Struct(*it)))); + if non_default_type_params_len == 0 { + // Fulfilled only if there are no generic parameters + lookup.mark_fulfilled(ScopeDef::ModuleDef(ModuleDef::Adt( + Adt::Struct(*it), + ))); + } lookup.insert(struct_ty.clone(), struct_exprs.iter().cloned()); Some((struct_ty, struct_exprs)) @@ -525,14 +533,17 @@ pub(super) fn impl_method<'a, DB: HirDatabase>( return None; } - let non_default_type_params_len = imp_type_params - .iter() - .chain(fn_type_params.iter()) - .filter(|it| it.default(db).is_none()) - .count(); + // Double check that we have fully known type + if ty.type_arguments().any(|it| it.contains_unknown()) { + return None; + } - // Ignore bigger number of generics for now as they kill the performance - if non_default_type_params_len > 0 { + let non_default_fn_type_params_len = + fn_type_params.iter().filter(|it| it.default(db).is_none()).count(); + + // Ignore functions with generics for now as they kill the performance + // Also checking bounds for generics is problematic + if non_default_fn_type_params_len > 0 { return None; } @@ -540,23 +551,23 @@ pub(super) fn impl_method<'a, DB: HirDatabase>( .iter_types() .collect::>() // Force take ownership .into_iter() - .permutations(non_default_type_params_len); + .permutations(non_default_fn_type_params_len); let exprs: Vec<_> = generic_params .filter_map(|generics| { // Insert default type params let mut g = generics.into_iter(); - let generics: Vec<_> = imp_type_params - .iter() - .chain(fn_type_params.iter()) - .map(|it| match it.default(db) { + let generics: Vec<_> = ty + .type_arguments() + .map(Some) + .chain(fn_type_params.iter().map(|it| match it.default(db) { Some(ty) => Some(ty), None => { let generic = g.next().expect("Missing type param"); // Filter out generics that do not unify due to trait bounds it.ty(db).could_unify_with(db, &generic).then_some(generic) } - }) + })) .collect::>()?; let ret_ty = it.ret_type_with_args( @@ -713,7 +724,8 @@ pub(super) fn impl_static_method<'a, DB: HirDatabase>( let db = ctx.sema.db; let module = ctx.scope.module(); lookup - .take_types_wishlist() + .types_wishlist() + .clone() .into_iter() .chain(iter::once(ctx.goal.clone())) .flat_map(|ty| { @@ -768,14 +780,17 @@ pub(super) fn impl_static_method<'a, DB: HirDatabase>( return None; } - let non_default_type_params_len = imp_type_params - .iter() - .chain(fn_type_params.iter()) - .filter(|it| it.default(db).is_none()) - .count(); + // Double check that we have fully known type + if ty.type_arguments().any(|it| it.contains_unknown()) { + return None; + } - // Ignore bigger number of generics for now as they kill the performance - if non_default_type_params_len > 1 { + let non_default_fn_type_params_len = + fn_type_params.iter().filter(|it| it.default(db).is_none()).count(); + + // Ignore functions with generics for now as they kill the performance + // Also checking bounds for generics is problematic + if non_default_fn_type_params_len > 0 { return None; } @@ -783,16 +798,16 @@ pub(super) fn impl_static_method<'a, DB: HirDatabase>( .iter_types() .collect::>() // Force take ownership .into_iter() - .permutations(non_default_type_params_len); + .permutations(non_default_fn_type_params_len); let exprs: Vec<_> = generic_params .filter_map(|generics| { // Insert default type params let mut g = generics.into_iter(); - let generics: Vec<_> = imp_type_params - .iter() - .chain(fn_type_params.iter()) - .map(|it| match it.default(db) { + let generics: Vec<_> = ty + .type_arguments() + .map(Some) + .chain(fn_type_params.iter().map(|it| match it.default(db) { Some(ty) => Some(ty), None => { let generic = g.next().expect("Missing type param"); @@ -802,7 +817,7 @@ pub(super) fn impl_static_method<'a, DB: HirDatabase>( // Filter out generics that do not unify due to trait bounds it.ty(db).could_unify_with(db, &generic).then_some(generic) } - }) + })) .collect::>()?; let ret_ty = it.ret_type_with_args( diff --git a/crates/ide-assists/src/handlers/term_search.rs b/crates/ide-assists/src/handlers/term_search.rs index 51a1a406f3..fa32a3bbe7 100644 --- a/crates/ide-assists/src/handlers/term_search.rs +++ b/crates/ide-assists/src/handlers/term_search.rs @@ -57,11 +57,14 @@ pub(crate) fn term_search(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option< }) .unique(); + let macro_name = macro_call.name(ctx.sema.db); + let macro_name = macro_name.display(ctx.sema.db); + for code in paths { acc.add_group( &GroupLabel(String::from("Term search")), AssistId("term_search", AssistKind::Generate), - format!("Replace todo!() with {code}"), + format!("Replace {macro_name}!() with {code}"), goal_range, |builder| { builder.replace(goal_range, code); diff --git a/crates/ide-completion/src/render.rs b/crates/ide-completion/src/render.rs index 3f374b307f..6d1a5a0bc5 100644 --- a/crates/ide-completion/src/render.rs +++ b/crates/ide-completion/src/render.rs @@ -2599,6 +2599,7 @@ fn foo() { expect![[r#" lc foo [type+local] ex foo [type] + ex Foo::B [type] ev Foo::A(…) [type_could_unify] ev Foo::B [type_could_unify] en Foo [type_could_unify] From be6f8e2648d99d8e858710460c89e9cf26685f00 Mon Sep 17 00:00:00 2001 From: Tavo Annus Date: Sun, 25 Feb 2024 17:24:07 +0200 Subject: [PATCH 2/3] Add `make_tuple` tactic --- crates/hir/src/lib.rs | 4 +- crates/hir/src/term_search.rs | 1 + crates/hir/src/term_search/tactics.rs | 58 +++++++++++++++++++ .../ide-assists/src/handlers/term_search.rs | 20 +++++++ 4 files changed, 82 insertions(+), 1 deletion(-) diff --git a/crates/hir/src/lib.rs b/crates/hir/src/lib.rs index 14066dee5f..f157b11df5 100644 --- a/crates/hir/src/lib.rs +++ b/crates/hir/src/lib.rs @@ -4321,8 +4321,10 @@ impl Type { self.ty .strip_references() .as_adt() + .map(|(_, substs)| substs) + .or_else(|| self.ty.strip_references().as_tuple()) .into_iter() - .flat_map(|(_, substs)| substs.iter(Interner)) + .flat_map(|substs| substs.iter(Interner)) .filter_map(|arg| arg.ty(Interner).cloned()) .map(move |ty| self.derived(ty)) } diff --git a/crates/hir/src/term_search.rs b/crates/hir/src/term_search.rs index 68244b1272..93e7300491 100644 --- a/crates/hir/src/term_search.rs +++ b/crates/hir/src/term_search.rs @@ -311,6 +311,7 @@ pub fn term_search(ctx: &TermSearchCtx<'_, DB>) -> Vec { solutions.extend(tactics::impl_method(ctx, &defs, &mut lookup)); solutions.extend(tactics::struct_projection(ctx, &defs, &mut lookup)); solutions.extend(tactics::impl_static_method(ctx, &defs, &mut lookup)); + solutions.extend(tactics::make_tuple(ctx, &defs, &mut lookup)); // Discard not interesting `ScopeDef`s for speedup for def in lookup.exhausted_scopedefs() { diff --git a/crates/hir/src/term_search/tactics.rs b/crates/hir/src/term_search/tactics.rs index 93a780d470..102e0ca4c3 100644 --- a/crates/hir/src/term_search/tactics.rs +++ b/crates/hir/src/term_search/tactics.rs @@ -872,3 +872,61 @@ pub(super) fn impl_static_method<'a, DB: HirDatabase>( .filter_map(|(ty, exprs)| ty.could_unify_with_deeply(db, &ctx.goal).then_some(exprs)) .flatten() } + +/// # Make tuple tactic +/// +/// Attempts to create tuple types if any are listed in types wishlist +/// +/// Updates lookup by new types reached and returns iterator that yields +/// elements that unify with `goal`. +/// +/// # Arguments +/// * `ctx` - Context for the term search +/// * `defs` - Set of items in scope at term search target location +/// * `lookup` - Lookup table for types +pub(super) fn make_tuple<'a, DB: HirDatabase>( + ctx: &'a TermSearchCtx<'a, DB>, + _defs: &'a FxHashSet, + lookup: &'a mut LookupTable, +) -> impl Iterator + 'a { + let db = ctx.sema.db; + let module = ctx.scope.module(); + + lookup + .types_wishlist() + .clone() + .into_iter() + .filter(|ty| ty.is_tuple()) + .filter_map(move |ty| { + // Double check to not contain unknown + if ty.contains_unknown() { + return None; + } + + // Ignore types that have something to do with lifetimes + if ctx.config.enable_borrowcheck && ty.contains_reference(db) { + return None; + } + + // Early exit if some param cannot be filled from lookup + let param_exprs: Vec> = + ty.type_arguments().map(|field| lookup.find(db, &field)).collect::>()?; + + let exprs: Vec = param_exprs + .into_iter() + .multi_cartesian_product() + .map(|params| { + let tys: Vec = params.iter().map(|it| it.ty(db)).collect(); + let tuple_ty = Type::new_tuple(module.krate().into(), &tys); + + let expr = Expr::Tuple { ty: tuple_ty.clone(), params }; + lookup.insert(tuple_ty, iter::once(expr.clone())); + expr + }) + .collect(); + + Some(exprs) + }) + .flatten() + .filter_map(|expr| expr.ty(db).could_unify_with_deeply(db, &ctx.goal).then_some(expr)) +} diff --git a/crates/ide-assists/src/handlers/term_search.rs b/crates/ide-assists/src/handlers/term_search.rs index fa32a3bbe7..0f4a8e3aec 100644 --- a/crates/ide-assists/src/handlers/term_search.rs +++ b/crates/ide-assists/src/handlers/term_search.rs @@ -253,4 +253,24 @@ fn g() { let a = &1; let b: f32 = f(a); }"#, fn g() { let a = &mut 1; let b: f32 = todo$0!(); }"#, ) } + + #[test] + fn test_tuple_simple() { + check_assist( + term_search, + r#"//- minicore: todo, unimplemented +fn f() { let a = 1; let b = 0.0; let c: (i32, f64) = todo$0!(); }"#, + r#"fn f() { let a = 1; let b = 0.0; let c: (i32, f64) = (a, b); }"#, + ) + } + + #[test] + fn test_tuple_nested() { + check_assist( + term_search, + r#"//- minicore: todo, unimplemented +fn f() { let a = 1; let b = 0.0; let c: (i32, (i32, f64)) = todo$0!(); }"#, + r#"fn f() { let a = 1; let b = 0.0; let c: (i32, (i32, f64)) = (a, (a, b)); }"#, + ) + } } From a2bf15eede888e2b010a1ed94bd4895d7c66c809 Mon Sep 17 00:00:00 2001 From: Tavo Annus Date: Sun, 25 Feb 2024 21:45:31 +0200 Subject: [PATCH 3/3] Filter out false positive errors --- crates/rust-analyzer/src/cli/analysis_stats.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/rust-analyzer/src/cli/analysis_stats.rs b/crates/rust-analyzer/src/cli/analysis_stats.rs index ce7e3b3cd6..186b65692e 100644 --- a/crates/rust-analyzer/src/cli/analysis_stats.rs +++ b/crates/rust-analyzer/src/cli/analysis_stats.rs @@ -453,8 +453,11 @@ impl flags::AnalysisStats { err_idx += 7; let err_code = &err[err_idx..err_idx + 4]; match err_code { - "0282" => continue, // Byproduct of testing method - "0277" if generated.contains(&todo) => continue, // See https://github.com/rust-lang/rust/issues/69882 + "0282" | "0283" => continue, // Byproduct of testing method + "0277" | "0308" if generated.contains(&todo) => continue, // See https://github.com/rust-lang/rust/issues/69882 + // FIXME: In some rare cases `AssocItem::container_or_implemented_trait` returns `None` for trait methods. + // Generated code is valid in case traits are imported + "0599" if err.contains("the following trait is implemented but not in scope") => continue, _ => (), } bar.println(err);