diff --git a/crates/hir/src/term_search/mod.rs b/crates/hir/src/term_search/mod.rs index b1e616e004c..6ea5b105def 100644 --- a/crates/hir/src/term_search/mod.rs +++ b/crates/hir/src/term_search/mod.rs @@ -12,25 +12,45 @@ mod tactics; +/// # Maximum amount of variations to take per type +/// +/// This is to speed up term search as there may be huge amount of variations of arguments for +/// function, even when the return type is always the same. The idea is to take first n and call it +/// a day. const MAX_VARIATIONS: usize = 10; +/// Key for lookup table to query new types reached. #[derive(Debug, Hash, PartialEq, Eq)] enum NewTypesKey { ImplMethod, StructProjection, } -/// Lookup table for term search +/// # Lookup table for term search +/// +/// Lookup table keeps all the state during term search. +/// This means it knows what types and how are reachable. +/// +/// The secondary functionality for lookup table is to keep track of new types reached since last +/// iteration as well as keeping track of which `ScopeDef` items have been used. +/// Both of them are to speed up the term search by leaving out types / ScopeDefs that likely do +/// not produce any new results. #[derive(Default, Debug)] struct LookupTable { + /// All the `TypeTree`s in "value" produce the type of "key" data: FxHashMap>, + /// New types reached since last query by the `NewTypesKey` new_types: FxHashMap>, + /// ScopeDefs that are not interesting any more exhausted_scopedefs: FxHashSet, + /// ScopeDefs that were used in current round round_scopedef_hits: FxHashSet, - scopedef_hits: FxHashMap, + /// Amount of rounds since scopedef was first used. + rounds_since_sopedef_hit: FxHashMap, } impl LookupTable { + /// Initialize lookup table fn new() -> Self { let mut res: Self = Default::default(); res.new_types.insert(NewTypesKey::ImplMethod, Vec::new()); @@ -38,6 +58,7 @@ fn new() -> Self { res } + /// Find all `TypeTree`s that unify with the `ty` fn find(&self, db: &dyn HirDatabase, ty: &Type) -> Option> { self.data .iter() @@ -45,6 +66,10 @@ fn find(&self, db: &dyn HirDatabase, ty: &Type) -> Option> { .map(|(_, tts)| tts.iter().cloned().collect()) } + /// 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 `TypeTree::Reference` and returns them. fn find_autoref(&self, db: &dyn HirDatabase, ty: &Type) -> Option> { self.data .iter() @@ -62,6 +87,11 @@ fn find_autoref(&self, db: &dyn HirDatabase, ty: &Type) -> Option> }) } + /// Insert new type trees for type + /// + /// Note that the types have to be the same, unification is not enough as unification is not + /// transitive. For example Vec and FxHashSet both unify with Iterator, + /// but they clearly do not unify themselves. fn insert(&mut self, ty: Type, trees: impl Iterator) { match self.data.get_mut(&ty) { Some(it) => it.extend(trees.take(MAX_VARIATIONS)), @@ -74,10 +104,14 @@ fn insert(&mut self, ty: Type, trees: impl Iterator) { } } + /// Iterate all the reachable types fn iter_types(&self) -> impl Iterator + '_ { self.data.keys().cloned() } + /// Query new types reached since last query by key + /// + /// Create new key if you wish to query it to avoid conflicting with existing queries. fn new_types(&mut self, key: NewTypesKey) -> Vec { match self.new_types.get_mut(&key) { Some(it) => std::mem::take(it), @@ -85,17 +119,24 @@ fn new_types(&mut self, key: NewTypesKey) -> Vec { } } + /// Mark `ScopeDef` as exhausted meaning it is not interesting for us any more fn mark_exhausted(&mut self, def: ScopeDef) { self.exhausted_scopedefs.insert(def); } + /// Mark `ScopeDef` as used meaning we managed to produce something useful from it fn mark_fulfilled(&mut self, def: ScopeDef) { self.round_scopedef_hits.insert(def); } + /// Start new round (meant to be called at the beginning of iteration in `term_search`) + /// + /// This functions marks some `ScopeDef`s as exhausted if there have been + /// `MAX_ROUNDS_AFTER_HIT` rounds after first using a `ScopeDef`. fn new_round(&mut self) { for def in &self.round_scopedef_hits { - let hits = self.scopedef_hits.entry(*def).and_modify(|n| *n += 1).or_insert(0); + let hits = + self.rounds_since_sopedef_hit.entry(*def).and_modify(|n| *n += 1).or_insert(0); const MAX_ROUNDS_AFTER_HIT: u32 = 2; if *hits > MAX_ROUNDS_AFTER_HIT { self.exhausted_scopedefs.insert(*def); @@ -104,6 +145,7 @@ fn new_round(&mut self) { self.round_scopedef_hits.clear(); } + /// Get exhausted `ScopeDef`s fn exhausted_scopedefs(&self) -> &FxHashSet { &self.exhausted_scopedefs } @@ -117,6 +159,22 @@ fn exhausted_scopedefs(&self) -> &FxHashSet { /// * `sema` - Semantics for the program /// * `scope` - Semantic scope, captures context for the term search /// * `goal` - Target / expected output type +/// +/// Internally this function uses Breadth First Search to find path to `goal` type. +/// The general idea is following: +/// 1. Populate lookup (frontier for BFS) from values (local variables, statics, constants, etc) +/// as well as from well knows values (such as `true/false` and `()`) +/// 2. Iteratively expand the frontier (or contents of the lookup) by trying different type +/// transformation tactics. For example functions take as from set of types (arguments) to some +/// type (return type). Other transformations include methods on type, type constructors and +/// projections to struct fields (field access). +/// 3. Once we manage to find path to type we are interested in we continue for single round to see +/// if we can find more paths that take us to the `goal` type. +/// 4. Return all the paths (type trees) that take us to the `goal` type. +/// +/// Note that there are usually more ways we can get to the `goal` type but some are discarded to +/// reduce the memory consumption. It is also unlikely anyone is willing ti browse through +/// thousands of possible responses so we currently take first 10 from every tactic. pub fn term_search( sema: &Semantics<'_, DB>, scope: &SemanticsScope<'_>, @@ -135,6 +193,7 @@ pub fn term_search( // Try trivial tactic first, also populates lookup table let mut solutions: Vec = tactics::trivial(sema.db, &defs, &mut lookup, goal).collect(); + // Use well known types tactic before iterations as it does not depend on other tactics solutions.extend(tactics::famous_types(sema.db, &module, &defs, &mut lookup, goal)); let mut solution_found = !solutions.is_empty(); @@ -147,12 +206,14 @@ pub fn term_search( solutions.extend(tactics::impl_method(sema.db, &module, &defs, &mut lookup, goal)); solutions.extend(tactics::struct_projection(sema.db, &module, &defs, &mut lookup, goal)); + // Break after 1 round after successful solution if solution_found { break; } solution_found = !solutions.is_empty(); + // Discard not interesting `ScopeDef`s for speedup for def in lookup.exhausted_scopedefs() { defs.remove(def); } diff --git a/crates/hir/src/term_search/tactics.rs b/crates/hir/src/term_search/tactics.rs index 87261617370..34ff420a814 100644 --- a/crates/hir/src/term_search/tactics.rs +++ b/crates/hir/src/term_search/tactics.rs @@ -1,4 +1,12 @@ //! Tactics for term search +//! +//! All the tactics take following arguments +//! * `db` - HIR database +//! * `module` - Module where the term search target location +//! * `defs` - Set of items in scope at term search target location +//! * `lookup` - Lookup table for types +//! * `goal` - Term search target type +//! And they return iterator that yields type trees that unify with the `goal` type. use hir_def::generics::TypeOrConstParamData; use hir_ty::db::HirDatabase; @@ -16,10 +24,21 @@ use super::{LookupTable, NewTypesKey, MAX_VARIATIONS}; -/// Trivial tactic +/// # Trivial tactic /// /// Attempts to fulfill the goal by trying items in scope -/// Also works as a starting point to move all items in scope to lookup table +/// Also works as a starting point to move all items in scope to lookup table. +/// +/// # Arguments +/// * `db` - HIR database +/// * `defs` - Set of items in scope at term search target location +/// * `lookup` - Lookup table for types +/// * `goal` - Term search target type +/// +/// Returns iterator that yields elements that unify with `goal`. +/// +/// _Note that there is no use of calling this tactic in every iteration as the output does not +/// depend on the current state of `lookup`_ pub(super) fn trivial<'a>( db: &'a dyn HirDatabase, defs: &'a FxHashSet, @@ -67,10 +86,13 @@ pub(super) fn trivial<'a>( }) } -/// Type constructor tactic +/// # Type constructor tactic /// /// Attempts different type constructors for enums and structs in scope /// +/// Updates lookup by new types reached and returns iterator that yields +/// elements that unify with `goal`. +/// /// # Arguments /// * `db` - HIR database /// * `module` - Module where the term search target location @@ -255,9 +277,13 @@ fn variant_helper( .flatten() } -/// Free function tactic +/// # Free function tactic /// -/// Attempts to call different functions in scope with parameters from lookup table +/// Attempts to call different functions in scope with parameters from lookup table. +/// Functions that include generics are not used for performance reasons. +/// +/// Updates lookup by new types reached and returns iterator that yields +/// elements that unify with `goal`. /// /// # Arguments /// * `db` - HIR database @@ -356,10 +382,15 @@ pub(super) fn free_function<'a>( .flatten() } -/// Impl method tactic +/// # Impl method tactic /// /// Attempts to to call methods on types from lookup table. /// This includes both functions from direct impl blocks as well as functions from traits. +/// Methods defined in impl blocks that are generic and methods that are themselves have +/// generics are ignored for performance reasons. +/// +/// Updates lookup by new types reached and returns iterator that yields +/// elements that unify with `goal`. /// /// # Arguments /// * `db` - HIR database @@ -484,9 +515,12 @@ pub(super) fn impl_method<'a>( .flatten() } -/// Struct projection tactic +/// # Struct projection tactic /// -/// Attempts different struct fields +/// Attempts different struct fields (`foo.bar.baz`) +/// +/// Updates lookup by new types reached and returns iterator that yields +/// elements that unify with `goal`. /// /// # Arguments /// * `db` - HIR database @@ -522,9 +556,14 @@ pub(super) fn struct_projection<'a>( .flatten() } -/// Famous types tactic +/// # Famous types tactic /// -/// Attempts different values of well known types such as `true` or `false` +/// Attempts different values of well known types such as `true` or `false`. +/// +/// Updates lookup by new types reached and returns iterator that yields +/// elements that unify with `goal`. +/// +/// _Note that there is no point of calling it iteratively as the output is always the same_ /// /// # Arguments /// * `db` - HIR database diff --git a/crates/hir/src/term_search/type_tree.rs b/crates/hir/src/term_search/type_tree.rs index 61b21c86eef..4178ba2d7df 100644 --- a/crates/hir/src/term_search/type_tree.rs +++ b/crates/hir/src/term_search/type_tree.rs @@ -9,6 +9,7 @@ Struct, StructKind, Trait, Type, Variant, }; +/// Helper function to prefix items with modules when required fn mod_item_path(db: &dyn HirDatabase, sema_scope: &SemanticsScope<'_>, def: &ModuleDef) -> String { // Account for locals shadowing items from module let name_hit_count = def.name(db).map(|def_name| { @@ -76,6 +77,11 @@ pub enum TypeTree { } impl TypeTree { + /// Generate source code for type tree. + /// + /// Note that trait imports are not added to generated code. + /// To make sure that the code is valid, callee has to also ensure that all the traits listed + /// by `traits_used` method are also imported. pub fn gen_source_code(&self, sema_scope: &SemanticsScope<'_>) -> String { let db = sema_scope.db; match self { @@ -222,6 +228,7 @@ pub fn ty(&self, db: &dyn HirDatabase) -> Type { } } + /// List the traits used in type tree pub fn traits_used(&self, db: &dyn HirDatabase) -> Vec { let mut res = Vec::new();