Skip to content

Commit 818c30c

Browse files
committed
Auto merge of rust-lang#16092 - kilpkonn:term_search_1, r=Veykril
feat: Introduce term search to rust-analyzer # Introduce term search to `rust-analyzer` _I've marked this as draft as there might be some shortcomings, please point them out so I can fix them. Otherwise I think it is kind of ready as I think I'll rather introduce extra functionality in follow up PRs._ Term search (or I guess expression search for rust) is a technique to generate code by basically making the types match. Consider the following program ```rust fn wrap(arg: i32) -> Option<i32> { todo!(); } ``` From the types of values in scope and constructors of `Option`, we can produce the expected result of wrapping the argument in `Option` Dependently typed languages such as `Idris2` and `Agda` have similar tools to help with proofs, but this can be also used in everyday development as a "auto-complete". # Demo videos https://github.com/rust-lang/rust-analyzer/assets/19900308/7b68a1b7-7dba-4e31-9221-6c7485e77d88 https://github.com/rust-lang/rust-analyzer/assets/19900308/0fae530a-aabb-4b28-af71-e19f8d3d64b2 # What does it currently do - It works well with locals, free functions, type constructors and non-static impl methods that take items by value. - Works with functions/methods that take shared references, but not with unique references (very conservative). - Can handle projections to struct fields (eg. `foo.bar.baz`) but this might me more conservative than it has to be to avoid conflicting with borrow checker - Should create only valid programs (no type / borrow checking errors). Tested with `rust-analyzer analysis-stats /path/to/ripgrep/Cargo.toml --run-term-search --validate-term-search` (basically running `cargo check` on all of the generated programs and only error seems to be due to type inference which is more of issue of testing method. # Performace / fitness ```txt ripgrep (latest) Tail Expr syntactic hits: 130/1692 (7%) Tail Exprs found: 523/1692 (30%) Term search avg time: 9ms Term search: 15.64s, 97ginstr, 8mb rust-analyzer (on this branch) Tail Expr syntactic hits: 804/13860 (5%) Tail Exprs found: 6757/13860 (48%) Term search avg time: 78ms Term search: 1088.23s, 6765ginstr, 98mb ``` Highly generic code seems to blow up the search space so currently the amount of generics allowed is functions/methods is limited down to 0 (1 didn't give much improvement and 2 is already like 0.5+s search time) # Plans for the future (not in this PR) - ``~~Add impl methods that do not take `self` type (should be quite straight forward)~~ Done - Be smarter (aka less restrictive) about borrow checking - this seems quite hard but since the current approach is rather naive I think some easy improvement is available. - ``~~See if it works as a autocomplete while typing~~ Done _Feel free to ask questions / point of shortcoming either here or on Zulip, I'll be happy to address them. I'm doing this as part of my MSc thesis so I'll be working on it till summer anyway 😄_
2 parents 47b4dd7 + 1257913 commit 818c30c

File tree

36 files changed

+3171
-150
lines changed

36 files changed

+3171
-150
lines changed

crates/hir-def/src/attr.rs

+27-15
Original file line numberDiff line numberDiff line change
@@ -377,27 +377,39 @@ impl AttrsWithOwner {
377377
AttrDefId::GenericParamId(it) => match it {
378378
GenericParamId::ConstParamId(it) => {
379379
let src = it.parent().child_source(db);
380-
RawAttrs::from_attrs_owner(
381-
db.upcast(),
382-
src.with_value(&src.value[it.local_id()]),
383-
db.span_map(src.file_id).as_ref(),
384-
)
380+
// FIXME: We should be never getting `None` here.
381+
match src.value.get(it.local_id()) {
382+
Some(val) => RawAttrs::from_attrs_owner(
383+
db.upcast(),
384+
src.with_value(val),
385+
db.span_map(src.file_id).as_ref(),
386+
),
387+
None => RawAttrs::EMPTY,
388+
}
385389
}
386390
GenericParamId::TypeParamId(it) => {
387391
let src = it.parent().child_source(db);
388-
RawAttrs::from_attrs_owner(
389-
db.upcast(),
390-
src.with_value(&src.value[it.local_id()]),
391-
db.span_map(src.file_id).as_ref(),
392-
)
392+
// FIXME: We should be never getting `None` here.
393+
match src.value.get(it.local_id()) {
394+
Some(val) => RawAttrs::from_attrs_owner(
395+
db.upcast(),
396+
src.with_value(val),
397+
db.span_map(src.file_id).as_ref(),
398+
),
399+
None => RawAttrs::EMPTY,
400+
}
393401
}
394402
GenericParamId::LifetimeParamId(it) => {
395403
let src = it.parent.child_source(db);
396-
RawAttrs::from_attrs_owner(
397-
db.upcast(),
398-
src.with_value(&src.value[it.local_id]),
399-
db.span_map(src.file_id).as_ref(),
400-
)
404+
// FIXME: We should be never getting `None` here.
405+
match src.value.get(it.local_id) {
406+
Some(val) => RawAttrs::from_attrs_owner(
407+
db.upcast(),
408+
src.with_value(val),
409+
db.span_map(src.file_id).as_ref(),
410+
),
411+
None => RawAttrs::EMPTY,
412+
}
401413
}
402414
},
403415
AttrDefId::ExternBlockId(it) => attrs_from_item_tree_loc(db, it),

crates/hir-ty/src/infer.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ use crate::{
6868
#[allow(unreachable_pub)]
6969
pub use coerce::could_coerce;
7070
#[allow(unreachable_pub)]
71-
pub use unify::could_unify;
71+
pub use unify::{could_unify, could_unify_deeply};
7272

7373
use cast::CastCheck;
7474
pub(crate) use closure::{CaptureKind, CapturedItem, CapturedItemWithoutTy};

crates/hir-ty/src/infer/unify.rs

+64-19
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ impl<T: HasInterner<Interner = Interner>> Canonicalized<T> {
7474
}
7575
}
7676

77+
/// Check if types unify.
78+
///
79+
/// Note that we consider placeholder types to unify with everything.
80+
/// This means that there may be some unresolved goals that actually set bounds for the placeholder
81+
/// type for the types to unify. For example `Option<T>` and `Option<U>` unify although there is
82+
/// unresolved goal `T = U`.
7783
pub fn could_unify(
7884
db: &dyn HirDatabase,
7985
env: Arc<TraitEnvironment>,
@@ -82,21 +88,35 @@ pub fn could_unify(
8288
unify(db, env, tys).is_some()
8389
}
8490

91+
/// Check if types unify eagerly making sure there are no unresolved goals.
92+
///
93+
/// This means that placeholder types are not considered to unify if there are any bounds set on
94+
/// them. For example `Option<T>` and `Option<U>` do not unify as we cannot show that `T = U`
95+
pub fn could_unify_deeply(
96+
db: &dyn HirDatabase,
97+
env: Arc<TraitEnvironment>,
98+
tys: &Canonical<(Ty, Ty)>,
99+
) -> bool {
100+
let mut table = InferenceTable::new(db, env);
101+
let vars = make_substitutions(tys, &mut table);
102+
let ty1_with_vars = vars.apply(tys.value.0.clone(), Interner);
103+
let ty2_with_vars = vars.apply(tys.value.1.clone(), Interner);
104+
let ty1_with_vars = table.normalize_associated_types_in(ty1_with_vars);
105+
let ty2_with_vars = table.normalize_associated_types_in(ty2_with_vars);
106+
table.resolve_obligations_as_possible();
107+
table.propagate_diverging_flag();
108+
let ty1_with_vars = table.resolve_completely(ty1_with_vars);
109+
let ty2_with_vars = table.resolve_completely(ty2_with_vars);
110+
table.unify_deeply(&ty1_with_vars, &ty2_with_vars)
111+
}
112+
85113
pub(crate) fn unify(
86114
db: &dyn HirDatabase,
87115
env: Arc<TraitEnvironment>,
88116
tys: &Canonical<(Ty, Ty)>,
89117
) -> Option<Substitution> {
90118
let mut table = InferenceTable::new(db, env);
91-
let vars = Substitution::from_iter(
92-
Interner,
93-
tys.binders.iter(Interner).map(|it| match &it.kind {
94-
chalk_ir::VariableKind::Ty(_) => table.new_type_var().cast(Interner),
95-
// FIXME: maybe wrong?
96-
chalk_ir::VariableKind::Lifetime => table.new_type_var().cast(Interner),
97-
chalk_ir::VariableKind::Const(ty) => table.new_const_var(ty.clone()).cast(Interner),
98-
}),
99-
);
119+
let vars = make_substitutions(tys, &mut table);
100120
let ty1_with_vars = vars.apply(tys.value.0.clone(), Interner);
101121
let ty2_with_vars = vars.apply(tys.value.1.clone(), Interner);
102122
if !table.unify(&ty1_with_vars, &ty2_with_vars) {
@@ -125,6 +145,21 @@ pub(crate) fn unify(
125145
))
126146
}
127147

148+
fn make_substitutions(
149+
tys: &chalk_ir::Canonical<(chalk_ir::Ty<Interner>, chalk_ir::Ty<Interner>)>,
150+
table: &mut InferenceTable<'_>,
151+
) -> chalk_ir::Substitution<Interner> {
152+
Substitution::from_iter(
153+
Interner,
154+
tys.binders.iter(Interner).map(|it| match &it.kind {
155+
chalk_ir::VariableKind::Ty(_) => table.new_type_var().cast(Interner),
156+
// FIXME: maybe wrong?
157+
chalk_ir::VariableKind::Lifetime => table.new_type_var().cast(Interner),
158+
chalk_ir::VariableKind::Const(ty) => table.new_const_var(ty.clone()).cast(Interner),
159+
}),
160+
)
161+
}
162+
128163
bitflags::bitflags! {
129164
#[derive(Default, Clone, Copy)]
130165
pub(crate) struct TypeVariableFlags: u8 {
@@ -431,6 +466,18 @@ impl<'a> InferenceTable<'a> {
431466
true
432467
}
433468

469+
/// Unify two relatable values (e.g. `Ty`) and check whether trait goals which arise from that could be fulfilled
470+
pub(crate) fn unify_deeply<T: ?Sized + Zip<Interner>>(&mut self, ty1: &T, ty2: &T) -> bool {
471+
let result = match self.try_unify(ty1, ty2) {
472+
Ok(r) => r,
473+
Err(_) => return false,
474+
};
475+
result.goals.iter().all(|goal| {
476+
let canonicalized = self.canonicalize(goal.clone());
477+
self.try_resolve_obligation(&canonicalized).is_some()
478+
})
479+
}
480+
434481
/// Unify two relatable values (e.g. `Ty`) and return new trait goals arising from it, so the
435482
/// caller needs to deal with them.
436483
pub(crate) fn try_unify<T: ?Sized + Zip<Interner>>(
@@ -501,7 +548,8 @@ impl<'a> InferenceTable<'a> {
501548

502549
fn register_obligation_in_env(&mut self, goal: InEnvironment<Goal>) {
503550
let canonicalized = self.canonicalize(goal);
504-
if !self.try_resolve_obligation(&canonicalized) {
551+
let solution = self.try_resolve_obligation(&canonicalized);
552+
if matches!(solution, Some(Solution::Ambig(_))) {
505553
self.pending_obligations.push(canonicalized);
506554
}
507555
}
@@ -627,38 +675,35 @@ impl<'a> InferenceTable<'a> {
627675
fn try_resolve_obligation(
628676
&mut self,
629677
canonicalized: &Canonicalized<InEnvironment<Goal>>,
630-
) -> bool {
678+
) -> Option<chalk_solve::Solution<Interner>> {
631679
let solution = self.db.trait_solve(
632680
self.trait_env.krate,
633681
self.trait_env.block,
634682
canonicalized.value.clone(),
635683
);
636684

637-
match solution {
685+
match &solution {
638686
Some(Solution::Unique(canonical_subst)) => {
639687
canonicalized.apply_solution(
640688
self,
641689
Canonical {
642-
binders: canonical_subst.binders,
690+
binders: canonical_subst.binders.clone(),
643691
// FIXME: handle constraints
644-
value: canonical_subst.value.subst,
692+
value: canonical_subst.value.subst.clone(),
645693
},
646694
);
647-
true
648695
}
649696
Some(Solution::Ambig(Guidance::Definite(substs))) => {
650-
canonicalized.apply_solution(self, substs);
651-
false
697+
canonicalized.apply_solution(self, substs.clone());
652698
}
653699
Some(_) => {
654700
// FIXME use this when trying to resolve everything at the end
655-
false
656701
}
657702
None => {
658703
// FIXME obligation cannot be fulfilled => diagnostic
659-
true
660704
}
661705
}
706+
solution
662707
}
663708

664709
pub(crate) fn callable_sig(

crates/hir-ty/src/lib.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ pub use builder::{ParamKind, TyBuilder};
7979
pub use chalk_ext::*;
8080
pub use infer::{
8181
closure::{CaptureKind, CapturedItem},
82-
could_coerce, could_unify, Adjust, Adjustment, AutoBorrow, BindingMode, InferenceDiagnostic,
83-
InferenceResult, OverloadedDeref, PointerCast,
82+
could_coerce, could_unify, could_unify_deeply, Adjust, Adjustment, AutoBorrow, BindingMode,
83+
InferenceDiagnostic, InferenceResult, OverloadedDeref, PointerCast,
8484
};
8585
pub use interner::Interner;
8686
pub use lower::{

0 commit comments

Comments
 (0)