Skip to main content

brink_driver/
lib.rs

1//! Pipeline orchestration for the brink ink compiler.
2//!
3//! `Driver` wraps a `ProjectDb` and provides higher-level operations:
4//! file discovery, analysis orchestration, diagnostic collection, and
5//! LIR input preparation. Both the compiler (one-shot) and LSP (long-lived)
6//! use `Driver` as their entry point.
7
8mod diagnostics;
9mod discover;
10
11use std::collections::HashMap;
12use std::io;
13
14pub use brink_analyzer::AnalysisResult;
15pub use brink_db::ProjectDb;
16pub use brink_ir::FileId;
17pub use diagnostics::DiagnosticReport;
18pub use discover::DiscoverError;
19
20/// Pipeline orchestration wrapper around `ProjectDb`.
21pub struct Driver {
22    db: ProjectDb,
23    analysis: Option<AnalysisResult>,
24}
25
26impl Driver {
27    /// Create a new driver with an empty database.
28    pub fn new() -> Self {
29        Self {
30            db: ProjectDb::new(),
31            analysis: None,
32        }
33    }
34
35    /// Create a driver from an existing database.
36    pub fn from_db(db: ProjectDb) -> Self {
37        Self { db, analysis: None }
38    }
39
40    /// Borrow the underlying database.
41    pub fn db(&self) -> &ProjectDb {
42        &self.db
43    }
44
45    /// Mutably borrow the underlying database.
46    ///
47    /// Invalidates the cached analysis result, since any mutation may change
48    /// HIR/manifest data that analysis depends on.
49    pub fn db_mut(&mut self) -> &mut ProjectDb {
50        self.analysis = None;
51        &mut self.db
52    }
53
54    /// Consume the driver and return the underlying database.
55    pub fn into_db(self) -> ProjectDb {
56        self.db
57    }
58
59    // ── Discovery ────────────────────────────────────────────────────
60
61    /// Discover all files reachable via INCLUDEs from the entry point.
62    pub fn discover<F>(&mut self, entry: &str, read_file: F) -> Result<(), DiscoverError>
63    where
64        F: FnMut(&str) -> Result<String, io::Error>,
65    {
66        discover::discover(&mut self.db, entry, &mut { read_file })
67    }
68
69    // ── Analysis ─────────────────────────────────────────────────────
70
71    /// Run cross-file analysis on all files (or return cached result).
72    #[expect(
73        clippy::expect_used,
74        reason = "analysis is always Some after the if-block above"
75    )]
76    pub fn analyze(&mut self) -> &AnalysisResult {
77        if self.analysis.is_none() {
78            let inputs = self.db.analysis_inputs();
79            let files: Vec<_> = inputs
80                .iter()
81                .map(|(id, hir, manifest)| (*id, hir, manifest))
82                .collect();
83
84            tracing::info!(files = files.len(), "running cross-file analysis");
85            self.analysis = Some(brink_analyzer::analyze(&files));
86        }
87        self.analysis.as_ref().expect("just set above")
88    }
89
90    /// Run analysis on a specific subset of files (one project). Not cached.
91    pub fn analyze_project(&self, file_ids: &[FileId]) -> AnalysisResult {
92        let inputs = self.db.analysis_inputs_for(file_ids);
93        let file_refs: Vec<_> = inputs
94            .iter()
95            .map(|(id, hir, manifest)| (*id, hir, manifest))
96            .collect();
97        brink_analyzer::analyze(&file_refs)
98    }
99
100    /// Snapshot analysis inputs for a subset of files.
101    pub fn analysis_inputs_for(
102        &self,
103        file_ids: &[FileId],
104    ) -> Vec<(FileId, brink_ir::HirFile, brink_ir::SymbolManifest)> {
105        self.db.analysis_inputs_for(file_ids)
106    }
107
108    /// Snapshot all analysis inputs.
109    pub fn analysis_inputs(&self) -> Vec<(FileId, brink_ir::HirFile, brink_ir::SymbolManifest)> {
110        self.db.analysis_inputs()
111    }
112
113    // ── Project graph ────────────────────────────────────────────────
114
115    /// Compute independent projects from include relationships.
116    pub fn compute_projects(&self) -> Vec<(FileId, Vec<FileId>)> {
117        self.db.compute_projects()
118    }
119
120    /// Return file IDs in topological include order.
121    pub fn file_ids_topo(&self, entry: FileId) -> Vec<FileId> {
122        self.db.file_ids_topo(entry)
123    }
124
125    /// Snapshot file metadata for diagnostic publishing.
126    pub fn file_metadata(&self) -> Vec<(FileId, String, String)> {
127        self.db.file_metadata()
128    }
129
130    // ── Diagnostics ──────────────────────────────────────────────────
131
132    /// Collect all diagnostics (lowering + analysis), apply suppressions, partition.
133    pub fn collect_diagnostics(
134        &self,
135        analysis: &AnalysisResult,
136        entry: Option<FileId>,
137    ) -> DiagnosticReport {
138        diagnostics::collect_diagnostics(&self.db, analysis, entry)
139    }
140
141    // ── LIR preparation ─────────────────────────────────────────────
142
143    /// Prepare inputs for LIR lowering.
144    ///
145    /// Returns HIR files in topological order and a path map for diagnostics.
146    pub fn lir_inputs(
147        &self,
148        entry: FileId,
149    ) -> (Vec<(FileId, &brink_ir::HirFile)>, HashMap<FileId, String>) {
150        let ids = self.file_ids_topo(entry);
151        let files: Vec<_> = ids
152            .into_iter()
153            .filter_map(|id| self.db.hir(id).map(|hir| (id, hir)))
154            .collect();
155        let paths: HashMap<_, _> = files
156            .iter()
157            .filter_map(|(id, _)| self.db.file_path(*id).map(|p| (*id, p.to_string())))
158            .collect();
159        (files, paths)
160    }
161}
162
163impl Default for Driver {
164    fn default() -> Self {
165        Self::new()
166    }
167}