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::{AnalysisOptions, 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    analysis_options: AnalysisOptions,
25}
26
27impl Driver {
28    /// Create a new driver with an empty database.
29    pub fn new() -> Self {
30        Self {
31            db: ProjectDb::new(),
32            analysis: None,
33            analysis_options: AnalysisOptions::default(),
34        }
35    }
36
37    /// Create a driver from an existing database.
38    pub fn from_db(db: ProjectDb) -> Self {
39        Self {
40            db,
41            analysis: None,
42            analysis_options: AnalysisOptions::default(),
43        }
44    }
45
46    /// Set the analysis options (e.g. a registered host manifest + external
47    /// check severity) used by [`analyze`](Self::analyze). Invalidates any
48    /// cached analysis.
49    pub fn set_analysis_options(&mut self, options: AnalysisOptions) {
50        self.analysis_options = options;
51        self.analysis = None;
52    }
53
54    /// Borrow the underlying database.
55    pub fn db(&self) -> &ProjectDb {
56        &self.db
57    }
58
59    /// Mutably borrow the underlying database.
60    ///
61    /// Invalidates the cached analysis result, since any mutation may change
62    /// HIR/manifest data that analysis depends on.
63    pub fn db_mut(&mut self) -> &mut ProjectDb {
64        self.analysis = None;
65        &mut self.db
66    }
67
68    /// Consume the driver and return the underlying database.
69    pub fn into_db(self) -> ProjectDb {
70        self.db
71    }
72
73    // ── Discovery ────────────────────────────────────────────────────
74
75    /// Discover all files reachable via INCLUDEs from the entry point.
76    pub fn discover<F>(&mut self, entry: &str, read_file: F) -> Result<(), DiscoverError>
77    where
78        F: FnMut(&str) -> Result<String, io::Error>,
79    {
80        discover::discover(&mut self.db, entry, &mut { read_file })
81    }
82
83    // ── Analysis ─────────────────────────────────────────────────────
84
85    /// Run cross-file analysis on all files (or return cached result).
86    #[expect(
87        clippy::expect_used,
88        reason = "analysis is always Some after the if-block above"
89    )]
90    pub fn analyze(&mut self) -> &AnalysisResult {
91        if self.analysis.is_none() {
92            let inputs = self.db.analysis_inputs();
93            let files: Vec<_> = inputs
94                .iter()
95                .map(|(id, hir, manifest)| (*id, hir, manifest))
96                .collect();
97
98            tracing::info!(files = files.len(), "running cross-file analysis");
99            self.analysis = Some(brink_analyzer::analyze_with_options(
100                &files,
101                &self.analysis_options,
102            ));
103        }
104        self.analysis.as_ref().expect("just set above")
105    }
106
107    /// Run analysis on a specific subset of files (one project). Not cached.
108    pub fn analyze_project(&self, file_ids: &[FileId]) -> AnalysisResult {
109        let inputs = self.db.analysis_inputs_for(file_ids);
110        let file_refs: Vec<_> = inputs
111            .iter()
112            .map(|(id, hir, manifest)| (*id, hir, manifest))
113            .collect();
114        brink_analyzer::analyze(&file_refs)
115    }
116
117    /// Snapshot analysis inputs for a subset of files.
118    pub fn analysis_inputs_for(
119        &self,
120        file_ids: &[FileId],
121    ) -> Vec<(FileId, brink_ir::HirFile, brink_ir::SymbolManifest)> {
122        self.db.analysis_inputs_for(file_ids)
123    }
124
125    /// Snapshot all analysis inputs.
126    pub fn analysis_inputs(&self) -> Vec<(FileId, brink_ir::HirFile, brink_ir::SymbolManifest)> {
127        self.db.analysis_inputs()
128    }
129
130    // ── Project graph ────────────────────────────────────────────────
131
132    /// Compute independent projects from include relationships.
133    pub fn compute_projects(&self) -> Vec<(FileId, Vec<FileId>)> {
134        self.db.compute_projects()
135    }
136
137    /// Return file IDs in topological include order.
138    pub fn file_ids_topo(&self, entry: FileId) -> Vec<FileId> {
139        self.db.file_ids_topo(entry)
140    }
141
142    /// Snapshot file metadata for diagnostic publishing.
143    pub fn file_metadata(&self) -> Vec<(FileId, String, String)> {
144        self.db.file_metadata()
145    }
146
147    // ── Diagnostics ──────────────────────────────────────────────────
148
149    /// Collect all diagnostics (lowering + analysis), apply suppressions, partition.
150    pub fn collect_diagnostics(
151        &self,
152        analysis: &AnalysisResult,
153        entry: Option<FileId>,
154    ) -> DiagnosticReport {
155        diagnostics::collect_diagnostics(&self.db, analysis, entry)
156    }
157
158    // ── LIR preparation ─────────────────────────────────────────────
159
160    /// Prepare inputs for LIR lowering.
161    ///
162    /// Returns HIR files in topological order and a path map for diagnostics.
163    pub fn lir_inputs(
164        &self,
165        entry: FileId,
166    ) -> (Vec<(FileId, &brink_ir::HirFile)>, HashMap<FileId, String>) {
167        let ids = self.file_ids_topo(entry);
168        let files: Vec<_> = ids
169            .into_iter()
170            .filter_map(|id| self.db.hir(id).map(|hir| (id, hir)))
171            .collect();
172        let paths: HashMap<_, _> = files
173            .iter()
174            .filter_map(|(id, _)| self.db.file_path(*id).map(|p| (*id, p.to_string())))
175            .collect();
176        (files, paths)
177    }
178}
179
180impl Default for Driver {
181    fn default() -> Self {
182        Self::new()
183    }
184}