Resolve names to all their candidate definitions#1259
Conversation
88e8c56 to
3bba52f
Compare
1b9493a to
d9c64fe
Compare
3bba52f to
10b4801
Compare
d9c64fe to
c08723b
Compare
10b4801 to
a6cc84b
Compare
DavisVaughan
left a comment
There was a problem hiding this comment.
A few medium importance level comments, but looks nice and matches my gut feeling about not using next() in File::resolve()
| @@ -164,8 +164,11 @@ fn test_last_def_in_sourced_file_wins() { | |||
| assert_matches!( | |||
| goto_definition(params, &state).unwrap(), | |||
| Some(GotoDefinitionResponse::Link(ref links)) => { | |||
| assert_eq!(links.len(), 2); | |||
| assert_eq!(links[0].target_uri, helpers_uri); | |||
| assert_eq!(links[0].target_range, range((1, 0), (1, 2))); | |||
| assert_eq!(links[0].target_range, range((0, 0), (0, 2))); | |||
| assert_eq!(links[1].target_uri, helpers_uri); | |||
| assert_eq!(links[1].target_range, range((1, 0), (1, 2))); | |||
There was a problem hiding this comment.
Is the idea that you aren't really going for technical accuracy here?
Like, from a technical perspective only the 2nd one is relevant.
But from a user experience perspective, I do think I would want to be made aware of both. Likely I have made some kind of mistake resulting in two fn of the same name.
Is that what you are thinking?
| let target_scope = self.resolve_super_target(name); | ||
|
|
||
| // A top-level `<<-` resolves its target to the file scope it already | ||
| // sits in (no enclosing frame to walk to), so the marker scope and the | ||
| // binding scope coincide. Record one definition carrying both flags, | ||
| // rather than pushing duplicate definition entries. | ||
| if target_scope == self.current_scope { |
There was a problem hiding this comment.
I was really hoping to see some comparison against file_scope here, for clarity.
Like, would it work to do this?
if self.current_scope == file_scope {
// go ahead and record that 1 def and then early exit
}
// now you do the target resolution
let target_scope = self.resolve_super_target(name);
or maybe even
match self.scopes[self.current_scope].parent {
Some(parent) => {
let target_scope = self.resolve_super_target(name, parent);
// remainder of this branch
// resolve_super_target would simplify to just the `loop`
}
None => {
// current scope is file scope, use that path
}
}
that actually feels quite nice to me
| // One definition, not a self-duplicate. | ||
| assert_eq!(index.definitions(file).len(), 1); | ||
| assert!(matches!( | ||
| index.definitions(file)[DefinitionId::from(0)].kind(), | ||
| DefinitionKind::SuperAssignment(_) | ||
| )); | ||
| assert!(matches!( | ||
| index.definitions(file)[DefinitionId::from(1)].kind(), | ||
| DefinitionKind::SuperAssignment(_) | ||
| )); | ||
| assert_eq!(index.uses(file).len(), 0); |
| pub fn exports(&self) -> FxHashMap<&str, Vec<(DefinitionId, &Definition)>> { | ||
| let file_scope = ScopeId::from(0); | ||
| let symbols = &self.symbol_tables[file_scope]; | ||
|
|
||
| let mut exports = FxHashMap::default(); | ||
| let mut exports: FxHashMap<&str, Vec<(DefinitionId, &Definition)>> = FxHashMap::default(); | ||
| for (id, def) in self.definitions[file_scope].iter() { | ||
| let name = symbols.symbol(def.symbol()).name(); | ||
| exports.insert(name, (id, def)); | ||
| exports.entry(name).or_default().push((id, def)); | ||
| } | ||
|
|
||
| exports |
There was a problem hiding this comment.
Here's a thought
What if this was
pub fn definitions_by_name(&self, scope: ScopeId) -> FxHashMap<&str, Vec<(DefinitionId, &Definition)>>that would go quite nicely alongside
pub fn definitions(&self, scope: ScopeId) -> &IndexVec<DefinitionId, Definition>which is the "flat" view of the exact same information
and would make it more useful outside just the file-scope
I personally also think this is "more accurate" now that we return EVERY definition and not just the last one.
Like, I think of the "exports" as being a set of unique names. And I can derive the "exports" from this set of definitions, but this set of definitions doesn't correspond to the exports to me.
It makes sense to me for SemanticIndex to provide the maximal amount of information, i.e. every definition. And callers can filter that down as required for their semantic use case. I just think the name doesn't quite line up now.
| let list = entries.entry(name.to_string()).or_default(); | ||
| for (_def_id, def) in defs { | ||
| let entry = match def.kind() { | ||
| DefinitionKind::Import { | ||
| file: target_url, | ||
| name: target_name, | ||
| .. | ||
| } => { | ||
| let target_url_id = FilePath::from_url(target_url); | ||
| let Some(target_file) = db.file_by_path(&target_url_id) else { | ||
| continue; | ||
| }; | ||
| ExportEntry::Import { | ||
| file: target_file, | ||
| name: target_name.clone(), | ||
| } | ||
| }, | ||
| _ => ExportEntry::Local, | ||
| }; | ||
| // Dedup, moving each entry to its last position so the final | ||
| // entry is the binding R picks at runtime (statements run in | ||
| // order, last write wins). `Local` carries no `def_id` (that | ||
| // contentlessness is what keeps `exports()` stable across body | ||
| // edits), so a name's several top-level definitions collapse to | ||
| // one `Local`, identical `source()` forwards to one `Import`, | ||
| // and `resolve_export` re-mints from the marker either way. | ||
| if let Some(pos) = list.iter().position(|e| e == &entry) { | ||
| list.remove(pos); | ||
| } | ||
| list.push(entry); |
There was a problem hiding this comment.
So IIUC Vec<ExportEntry> will have:
- At most 1
ExportEntry::Localentry - Possibly multiple
ExportEntry::Importentries if the samenamecame from multiple files
But ExportEntrys themselves are deduplicated based on equality
| @@ -60,27 +64,42 @@ impl File { | |||
| /// chains by returning empty exports for every cycle participant. | |||
| #[salsa::tracked(returns(ref), cycle_result = exports_cycle_result)] | |||
| pub fn exports(self, db: &dyn Db) -> FileExports { | |||
There was a problem hiding this comment.
Is resolve_source()'s usage of exports() altered in any way due to these changes?
| // The `Local` marker doesn't carry a `def_id`, so recover | ||
| // every file-scope `def_id` for the name from the semantic | ||
| // index and mint each through the single site. A name bound | ||
| // more than once at top level fans out here. |
There was a problem hiding this comment.
I admittedly do feel a bit weird about the dance we are doing with Local here:
- Find all exports
- Collapse multiple into 1
Local - Rediscover all exports for that
Localhere
I saw the note about "keeps exports() stable across body edits, so a name's several top-level definitions collapse", but how common is that really?
I would trade a little performance for not doing the collapse and rediscover dance.
Plus, surely rediscovering here also takes some time too, right? And maybe we'd gain back what we lost?
Branched from #1258
Progress towards #1212
Symbol resolvers that don't take cursor offsets (
File::resolve(),resolve_at(), etc) now return all potential candidates, rather than the last one. The goto-def, find-refs, and rename handlers now offer all of them.Candidates come back in definition order, with R's runtime winner (last assignment wins) as the last element, and that order is preserved across
source()imports.A top-level
<<-now records a single file-scope definition carrying bothIS_SUPER_BOUNDandIS_BOUND,rather than a marker plus a same-scope target. At file scope there's no enclosing frame to walk to, so the two coincide. This avoids a spurious duplicate candidate.