1use std::collections::HashMap;
2
3use brink_ir::suppressions::{Suppressions, parse_suppressions};
4use brink_ir::{
5 Diagnostic, FileId, HirFile, SymbolManifest, lower, lower_single_knot, lower_top_level,
6};
7use brink_syntax::ast::AstNode as _;
8use brink_syntax::{Parse, parse_with_cache};
9use rowan::{GreenNode, NodeCache};
10use tracing::debug;
11
12use crate::file_state::{FileState, TopLevelEntry};
13use crate::include_graph::IncludeGraph;
14use crate::knot_cache::KnotEntry;
15
16pub struct ProjectDb {
22 files: HashMap<FileId, FileState>,
23 path_to_id: HashMap<String, FileId>,
24 id_to_path: HashMap<FileId, String>,
25 next_id: u32,
26 include_graph: IncludeGraph,
27 node_cache: NodeCache,
28}
29
30impl ProjectDb {
31 pub fn new() -> Self {
33 Self {
34 files: HashMap::new(),
35 path_to_id: HashMap::new(),
36 id_to_path: HashMap::new(),
37 next_id: 0,
38 include_graph: IncludeGraph::new(),
39 node_cache: NodeCache::default(),
40 }
41 }
42
43 pub fn set_file(&mut self, path: &str, source: String) -> FileId {
45 let file_id = self.get_or_create_id(path);
46
47 let parse = parse_with_cache(&source, &mut self.node_cache);
48 let tree = parse.tree();
49
50 let knot_entries: Vec<KnotEntry> = tree
52 .knots()
53 .map(|knot_ast| {
54 let green = knot_ast.syntax().green().into();
55 let offset = knot_ast.syntax().text_range().start();
56 let (knot, manifest, diagnostics) = lower_single_knot(file_id, &knot_ast);
57 KnotEntry {
58 green,
59 offset,
60 knot,
61 manifest,
62 diagnostics,
63 }
64 })
65 .collect();
66
67 let top_level = Self::lower_top_level_entry(file_id, &tree);
69
70 let (hir, manifest, diagnostics) =
72 Self::assemble(file_id, &knot_entries, &top_level, &tree);
73
74 let suppressions = parse_suppressions(&source);
75
76 let state = FileState {
77 source,
78 parse,
79 knot_entries,
80 top_level,
81 hir,
82 manifest,
83 diagnostics,
84 suppressions,
85 };
86
87 let include_ids: Vec<FileId> = state
89 .hir
90 .includes
91 .iter()
92 .filter_map(|inc| {
93 let resolved = resolve_include_path(path, &inc.file_path);
94 self.path_to_id.get(&resolved).copied()
95 })
96 .collect();
97 self.include_graph.update(file_id, include_ids);
98
99 self.files.insert(file_id, state);
100
101 debug!(path, id = file_id.0, "set_file complete");
102 file_id
103 }
104
105 pub fn update_file(&mut self, path: &str, source: String) -> FileId {
108 let file_id = self.get_or_create_id(path);
109
110 if !self.files.contains_key(&file_id) {
112 return self.set_file(path, source);
113 }
114
115 let parse = parse_with_cache(&source, &mut self.node_cache);
116 let tree = parse.tree();
117
118 let top_level = Self::lower_top_level_entry(file_id, &tree);
120
121 let new_knot_asts: Vec<_> = tree.knots().collect();
123 let old_state = self.files.get(&file_id);
124
125 let mut knot_entries = Vec::with_capacity(new_knot_asts.len());
126 let mut reused = 0u32;
127
128 for (i, knot_ast) in new_knot_asts.iter().enumerate() {
129 let new_green: GreenNode = knot_ast.syntax().green().into();
130
131 let new_offset = knot_ast.syntax().text_range().start();
132 let reuse_entry = old_state
133 .and_then(|s| s.knot_entries.get(i))
134 .filter(|old| old.green == new_green && old.offset == new_offset);
135
136 if let Some(old_entry) = reuse_entry {
137 knot_entries.push(KnotEntry {
138 green: new_green,
139 offset: new_offset,
140 knot: old_entry.knot.clone(),
141 manifest: old_entry.manifest.clone(),
142 diagnostics: old_entry.diagnostics.clone(),
143 });
144 reused += 1;
145 } else {
146 let (knot, manifest, diagnostics) = lower_single_knot(file_id, knot_ast);
147 knot_entries.push(KnotEntry {
148 green: new_green,
149 offset: new_offset,
150 knot,
151 manifest,
152 diagnostics,
153 });
154 }
155 }
156
157 debug!(
158 path,
159 total = new_knot_asts.len(),
160 reused,
161 "knot diff complete"
162 );
163
164 let (hir, manifest, diagnostics) =
165 Self::assemble(file_id, &knot_entries, &top_level, &tree);
166
167 let suppressions = parse_suppressions(&source);
168
169 let state = FileState {
170 source,
171 parse,
172 knot_entries,
173 top_level,
174 hir,
175 manifest,
176 diagnostics,
177 suppressions,
178 };
179
180 let include_ids: Vec<FileId> = state
182 .hir
183 .includes
184 .iter()
185 .filter_map(|inc| {
186 let resolved = resolve_include_path(path, &inc.file_path);
187 self.path_to_id.get(&resolved).copied()
188 })
189 .collect();
190 self.include_graph.update(file_id, include_ids);
191
192 self.files.insert(file_id, state);
193
194 file_id
195 }
196
197 pub fn remove_file(&mut self, path: &str) {
199 if let Some(id) = self.path_to_id.remove(path) {
200 self.id_to_path.remove(&id);
201 self.files.remove(&id);
202 self.include_graph.remove(id);
203 }
204 }
205
206 pub fn file_id(&self, path: &str) -> Option<FileId> {
208 self.path_to_id.get(path).copied()
209 }
210
211 pub fn file_path(&self, id: FileId) -> Option<&str> {
213 self.id_to_path.get(&id).map(String::as_str)
214 }
215
216 pub fn file_ids(&self) -> impl Iterator<Item = FileId> + '_ {
218 let mut ids: Vec<_> = self.files.keys().copied().collect();
219 ids.sort_by_key(|id| id.0);
220 ids.into_iter()
221 }
222
223 pub fn file_ids_topo(&self, entry: FileId) -> Vec<FileId> {
226 let all: Vec<_> = self.files.keys().copied().collect();
227 self.include_graph.topological_order(entry, &all)
228 }
229
230 pub fn parse(&self, id: FileId) -> Option<&Parse> {
232 self.files.get(&id).map(|s| &s.parse)
233 }
234
235 pub fn hir(&self, id: FileId) -> Option<&HirFile> {
237 self.files.get(&id).map(|s| &s.hir)
238 }
239
240 pub fn manifest(&self, id: FileId) -> Option<&SymbolManifest> {
242 self.files.get(&id).map(|s| &s.manifest)
243 }
244
245 pub fn source(&self, id: FileId) -> Option<&str> {
247 self.files.get(&id).map(|s| s.source.as_str())
248 }
249
250 pub fn file_diagnostics(&self, id: FileId) -> Option<&[Diagnostic]> {
252 self.files.get(&id).map(|s| s.diagnostics.as_slice())
253 }
254
255 pub fn suppressions(&self, id: FileId) -> Option<&Suppressions> {
257 self.files.get(&id).map(|s| &s.suppressions)
258 }
259
260 pub fn rebuild_include_graph(&mut self) {
266 let file_list: Vec<(FileId, String)> = self
267 .files
268 .keys()
269 .filter_map(|&id| self.id_to_path.get(&id).map(|p| (id, p.clone())))
270 .collect();
271
272 for (file_id, file_path) in &file_list {
273 if let Some(state) = self.files.get(file_id) {
274 let include_ids: Vec<FileId> = state
275 .hir
276 .includes
277 .iter()
278 .filter_map(|inc| {
279 let resolved = resolve_include_path(file_path, &inc.file_path);
280 self.path_to_id.get(&resolved).copied()
281 })
282 .collect();
283 self.include_graph.update(*file_id, include_ids);
284 }
285 }
286 }
287
288 pub fn find_cycle(&self) -> Option<Vec<FileId>> {
292 self.include_graph.find_cycle()
293 }
294
295 pub fn compute_projects(&self) -> Vec<(FileId, Vec<FileId>)> {
299 let all: Vec<_> = self.files.keys().copied().collect();
300 self.include_graph.compute_projects(&all)
301 }
302
303 pub fn analysis_inputs_for(
307 &self,
308 file_ids: &[FileId],
309 ) -> Vec<(FileId, HirFile, SymbolManifest)> {
310 let mut inputs: Vec<_> = file_ids
311 .iter()
312 .filter_map(|&id| {
313 let state = self.files.get(&id)?;
314 Some((id, state.hir.clone(), state.manifest.clone()))
315 })
316 .collect();
317 inputs.sort_by_key(|(id, _, _)| id.0);
318 inputs
319 }
320
321 pub fn analysis_inputs(&self) -> Vec<(FileId, HirFile, SymbolManifest)> {
326 let mut inputs: Vec<_> = self
327 .files
328 .iter()
329 .map(|(&id, state)| (id, state.hir.clone(), state.manifest.clone()))
330 .collect();
331 inputs.sort_by_key(|(id, _, _)| id.0);
332 inputs
333 }
334
335 pub fn file_metadata(&self) -> Vec<(FileId, String, String)> {
339 let mut meta: Vec<_> = self
340 .files
341 .keys()
342 .filter_map(|&id| {
343 let path = self.id_to_path.get(&id)?.clone();
344 let source = self.files.get(&id)?.source.clone();
345 Some((id, path, source))
346 })
347 .collect();
348 meta.sort_by_key(|(id, _, _)| id.0);
349 meta
350 }
351
352 fn get_or_create_id(&mut self, path: &str) -> FileId {
355 if let Some(&id) = self.path_to_id.get(path) {
356 return id;
357 }
358 let id = FileId(self.next_id);
359 self.next_id += 1;
360 self.path_to_id.insert(path.to_string(), id);
361 self.id_to_path.insert(id, path.to_string());
362 id
363 }
364
365 fn lower_top_level_entry(
366 file_id: FileId,
367 tree: &brink_syntax::ast::SourceFile,
368 ) -> TopLevelEntry {
369 let green_children = Self::collect_top_level_green(tree);
370 let (root_content, top_level_knots, manifest, diagnostics) = lower_top_level(file_id, tree);
371 TopLevelEntry {
372 green_children,
373 root_content,
374 top_level_knots,
375 manifest,
376 diagnostics,
377 }
378 }
379
380 fn collect_top_level_green(tree: &brink_syntax::ast::SourceFile) -> Vec<GreenNode> {
382 use brink_syntax::SyntaxKind;
383
384 tree.syntax()
385 .children()
386 .filter(|child| child.kind() != SyntaxKind::KNOT_DEF)
387 .map(|child| child.green().into())
388 .collect()
389 }
390
391 fn assemble(
393 file_id: FileId,
394 knot_entries: &[KnotEntry],
395 top_level: &TopLevelEntry,
396 tree: &brink_syntax::ast::SourceFile,
397 ) -> (HirFile, SymbolManifest, Vec<Diagnostic>) {
398 let (mut full_hir, _full_manifest, _full_diag) = lower(file_id, tree);
407
408 full_hir.knots = knot_entries.iter().filter_map(|e| e.knot.clone()).collect();
411 full_hir.knots.extend(top_level.top_level_knots.clone());
412 full_hir.root_content = top_level.root_content.clone();
413
414 let mut manifest = top_level.manifest.clone();
416 for entry in knot_entries {
417 merge_manifest_into(&mut manifest, &entry.manifest);
418 }
419
420 let mut diagnostics = top_level.diagnostics.clone();
422 for entry in knot_entries {
423 diagnostics.extend(entry.diagnostics.iter().cloned());
424 }
425
426 (full_hir, manifest, diagnostics)
427 }
428}
429
430impl Default for ProjectDb {
431 fn default() -> Self {
432 Self::new()
433 }
434}
435
436fn merge_manifest_into(dst: &mut SymbolManifest, src: &SymbolManifest) {
438 dst.knots.extend(src.knots.iter().cloned());
439 dst.stitches.extend(src.stitches.iter().cloned());
440 dst.variables.extend(src.variables.iter().cloned());
441 dst.constants.extend(src.constants.iter().cloned());
442 dst.lists.extend(src.lists.iter().cloned());
443 dst.externals.extend(src.externals.iter().cloned());
444 dst.labels.extend(src.labels.iter().cloned());
445 dst.list_items.extend(src.list_items.iter().cloned());
446 dst.locals.extend(src.locals.iter().cloned());
447 dst.unresolved.extend(src.unresolved.iter().cloned());
448}
449
450pub fn resolve_include_path(from_file: &str, include_path: &str) -> String {
456 match from_file.rfind('/') {
457 Some(i) => format!("{}/{include_path}", &from_file[..i]),
458 None => include_path.to_string(),
459 }
460}