Auto merge of #16687 - kilpkonn:master, r=Veykril

feat: Add "make tuple" tactic to term search

Follow up to https://github.com/rust-lang/rust-analyzer/pull/16092

Now term search also supports tuples.
```rust
let a: i32 = 1;
let b: f64 = 0.0;
let c: (i32, (f64, i32)) = todo!(); // Finds (a, (b, a))
```
In addition to new tactic that handles tuples I changed how the generics are handled.
Previously it tried all possible options from types we had in scope but now it only tries useful ones that help us directly towards the goal or at least towards calling some other function.
This changes O(2^n) to O(n^2) where n is amount of rounds which in practice allows using types that take generics for multiple rounds (previously limited to 1). Average case that also used to be exponential is now roughly linear.
This means that deeply nested generics also work.
````rust
// Finds all valid combos, including `Some(Some(Some(...)))`
let a: Option<Option<Option<bool>>> = todo!();
````

_Note that although the complexity is smaller allowing more types with generics the search overall slows down considerably. I hope it's fine tho as the autocomplete is disabled by default and for code actions it's not super slow. Might have to tweak the depth hyper parameter tho_

This resulted in a huge increase of results found (benchmarks on `ripgrep` crate):
Before
````
Tail Expr syntactic hits: 149/1692 (8%)
Tail Exprs found: 749/1692 (44%)
Term search avg time: 18ms
```
After
```
Tail Expr syntactic hits: 291/1692 (17%)
Tail Exprs found: 1253/1692 (74%)
Term search avg time: 139ms
````

Most changes are local to term search except some tuple related stuff on `hir::Type`.
This commit is contained in:
bors 2024-02-27 09:41:14 +00:00
commit 6b250a22c4
7 changed files with 223 additions and 76 deletions

View file

@ -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, ..))
}
@ -4320,8 +4325,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))
}

View file

@ -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<Vec<Expr>> {
self.data
fn find(&mut self, db: &dyn HirDatabase, ty: &Type) -> Option<Vec<Expr>> {
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<Vec<Expr>> {
self.data
fn find_autoref(&mut self, db: &dyn HirDatabase, ty: &Type) -> Option<Vec<Expr>> {
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<Item = Expr>) {
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<Type> {
std::mem::take(&mut self.types_wishlist)
fn types_wishlist(&mut self) -> &FxHashSet<Type> {
&self.types_wishlist
}
}
@ -272,7 +296,7 @@ pub fn term_search<DB: HirDatabase>(ctx: &TermSearchCtx<'_, DB>) -> Vec<Expr> {
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<Expr> = tactics::trivial(ctx, &defs, &mut lookup).collect();
@ -287,6 +311,7 @@ pub fn term_search<DB: HirDatabase>(ctx: &TermSearchCtx<'_, DB>) -> Vec<Expr> {
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() {

View file

@ -138,6 +138,8 @@ pub enum Expr {
Variant { variant: Variant, generics: Vec<Type>, params: Vec<Expr> },
/// Struct construction
Struct { strukt: Struct, generics: Vec<Type>, params: Vec<Expr> },
/// Tuple construction
Tuple { ty: Type, params: Vec<Expr> },
/// Struct field access
Field { expr: Box<Expr>, 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::<Result<Vec<String>, 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(),

View file

@ -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<Expr>)> {
// 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::<Vec<_>>() // 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::<Vec<Type>>())
.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::<Option<_>>()?;
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<Expr>)> = 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::<Vec<_>>() // 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::<Vec<Type>>())
.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::<Option<_>>()?;
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::<Vec<_>>() // 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::<Option<_>>()?;
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::<Vec<_>>() // 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::<Option<_>>()?;
let ret_ty = it.ret_type_with_args(
@ -857,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<ScopeDef>,
lookup: &'a mut LookupTable,
) -> impl Iterator<Item = Expr> + '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<Vec<Expr>> =
ty.type_arguments().map(|field| lookup.find(db, &field)).collect::<Option<_>>()?;
let exprs: Vec<Expr> = param_exprs
.into_iter()
.multi_cartesian_product()
.map(|params| {
let tys: Vec<Type> = 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))
}

View file

@ -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);
@ -250,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)); }"#,
)
}
}

View file

@ -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]

View file

@ -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);