lutra_compiler/project/analysis.rs
1use std::collections::HashMap;
2
3use crate::Span;
4use crate::pr::Path;
5
6use super::Project;
7
8/// Information about a symbol at a given source position.
9///
10/// Returned by [`Project::find_by_span`] and used by LSP features such as
11/// go-to-definition and hover.
12pub struct SymbolInfo {
13 /// Span of the identifier token at the queried position.
14 pub source_span: Span,
15 /// Path of the referenced global definition, or `None`
16 /// for local bindings.
17 pub target_path: Option<Path>,
18 /// Span of the definition site (go-to-definition target).
19 pub target_span: Option<Span>,
20}
21
22impl Project {
23 /// Look up the symbol at `(source_id, offset)` — a byte offset within a
24 /// source file — and return information about what it refers to.
25 ///
26 /// Returns `None` when the offset does not correspond to a known identifier.
27 pub fn find_by_span(&self, source_id: u16, offset: u32) -> Option<SymbolInfo> {
28 let (target, source_span) = self.target_map.find_at(source_id, offset)?;
29 Some(match target {
30 TargetSpan::Global(path) => {
31 let target_span = self.root_module.get(path).and_then(|d| d.span_name);
32 SymbolInfo {
33 source_span,
34 target_path: Some(path.clone()),
35 target_span,
36 }
37 }
38 TargetSpan::Span(target_span) => SymbolInfo {
39 source_span,
40 target_path: None,
41 target_span: Some(*target_span),
42 },
43 })
44 }
45}
46
47/// An index of all resolved identifier references in a compiled project,
48/// keyed by source location. Built once after name resolution and used for
49/// queries like go-to-definition.
50#[derive(Debug, Default)]
51pub struct TargetMap {
52 /// One sorted vec per source file (keyed by source_id).
53 /// Each vec is sorted by `TargetEntry::start` to allow binary search.
54 by_source: HashMap<u16, Vec<TargetEntry>>,
55}
56
57#[derive(Debug)]
58struct TargetEntry {
59 start: u32,
60 len: u16,
61 target: TargetSpan,
62}
63
64#[derive(Debug)]
65pub enum TargetSpan {
66 Global(Path),
67 Span(Span),
68}
69
70impl TargetMap {
71 /// Build a `TargetMap` from a flat list of `(span, target)` pairs collected
72 /// during name resolution. Overlay source IDs (`u16::MAX`) are excluded.
73 pub fn build(entries: Vec<(Span, TargetSpan)>) -> Self {
74 let mut by_source: HashMap<u16, Vec<TargetEntry>> = HashMap::new();
75
76 for (span, target) in entries {
77 let entry = by_source.entry(span.source_id).or_default();
78 entry.push(TargetEntry {
79 start: span.start,
80 len: span.len,
81 target,
82 });
83 }
84
85 for vec in by_source.values_mut() {
86 vec.sort_by_key(|e| e.start);
87 }
88
89 TargetMap { by_source }
90 }
91
92 /// Find the resolved target for the identifier at `(source_id, offset)`.
93 ///
94 /// Returns both the [`TargetSpan`] and the source [`Span`] of the matched
95 /// token (i.e. the span of the identifier itself, useful as the LSP hover
96 /// range).
97 ///
98 /// When multiple spans contain the offset (e.g. nested expressions share a
99 /// start position), the innermost one (smallest `len`) is returned.
100 pub fn find_at(&self, source_id: u16, offset: u32) -> Option<(&TargetSpan, Span)> {
101 let entries = self.by_source.get(&source_id)?;
102
103 // All entries with start <= offset are candidates; find the boundary.
104 let end = entries.partition_point(|e| e.start <= offset);
105
106 entries[..end]
107 .iter()
108 .filter(|e| e.start + e.len as u32 > offset)
109 .min_by_key(|e| e.len)
110 .map(|e| {
111 let span = Span {
112 source_id,
113 start: e.start,
114 len: e.len,
115 };
116 (&e.target, span)
117 })
118 }
119}