1use std::path::{Path, PathBuf};
12use std::sync::Arc;
13
14use gruel_compiler::{
15 FileId, JsonDiagnostic, MultiFileJsonFormatter, PreviewFeatures, SourceFile, SourceInfo, Type,
16 TypeInternPool, compile_frontend_from_ast_with_file_paths, merge_symbols,
17 parse_all_files_with_preview, prepend_prelude,
18};
19use gruel_parser::ast::Ast;
20use gruel_target::Target;
21use gruel_util::Span;
22use rustc_hash::FxHashMap;
23
24use crate::position::LineMap;
25use crate::workspace::build_root_closure;
26
27#[derive(Debug, Clone)]
30pub struct WorkspaceFile {
31 pub path: PathBuf,
32 pub text: String,
33 pub file_id: FileId,
36}
37
38#[derive(Debug)]
43pub struct Snapshot {
44 pub ast: Ast,
45 pub interner: Arc<lasso::ThreadedRodeo>,
50 pub sources: FxHashMap<FileId, WorkspaceFile>,
52 pub path_to_file_id: FxHashMap<PathBuf, FileId>,
54 pub line_maps: FxHashMap<FileId, LineMap>,
56 pub type_pool: Option<Arc<TypeInternPool>>,
59 pub expr_types: FxHashMap<Span, Type>,
63}
64
65pub struct AnalysisResult {
67 pub diagnostics: Vec<JsonDiagnostic>,
68 pub snapshot: Option<Snapshot>,
69}
70
71pub fn analyze_root<F>(
76 root: WorkspaceFile,
77 workspace_root: Option<&Path>,
78 preview_features: &PreviewFeatures,
79 target: &Target,
80 open_text: F,
81) -> AnalysisResult
82where
83 F: FnMut(&Path) -> Option<String>,
84{
85 let closure = build_root_closure(root, workspace_root, preview_features, open_text);
86 analyze(&closure, preview_features, target)
87}
88
89pub fn analyze(
92 files: &[WorkspaceFile],
93 preview_features: &PreviewFeatures,
94 target: &Target,
95) -> AnalysisResult {
96 if files.is_empty() {
97 return AnalysisResult {
98 diagnostics: vec![],
99 snapshot: None,
100 };
101 }
102
103 let sources: Vec<SourceFile<'_>> = files
105 .iter()
106 .map(|f| SourceFile::new(path_str(&f.path), f.text.as_str(), f.file_id))
107 .collect();
108
109 let source_infos: Vec<(FileId, SourceInfo<'_>)> = files
111 .iter()
112 .map(|f| {
113 (
114 f.file_id,
115 SourceInfo::new(f.text.as_str(), path_str(&f.path)),
116 )
117 })
118 .collect();
119 let formatter = MultiFileJsonFormatter::new(source_infos);
120
121 let mut diagnostics = Vec::new();
122
123 let parsed = match parse_all_files_with_preview(&sources, preview_features) {
125 Ok(p) => p,
126 Err(errors) => {
127 for e in errors.iter() {
128 diagnostics.push(formatter.format_error(e));
129 }
130 return AnalysisResult {
131 diagnostics,
132 snapshot: None,
133 };
134 }
135 };
136
137 let merged = match merge_symbols(parsed) {
139 Ok(m) => m,
140 Err(errors) => {
141 for e in errors.iter() {
142 diagnostics.push(formatter.format_error(e));
143 }
144 return AnalysisResult {
145 diagnostics,
146 snapshot: None,
147 };
148 }
149 };
150
151 let (ast_with_prelude, interner_with_prelude) =
156 match prepend_prelude(merged.ast, merged.interner, preview_features) {
157 Ok(p) => p,
158 Err(errors) => {
159 for e in errors.iter() {
160 diagnostics.push(formatter.format_error(e));
161 }
162 return AnalysisResult {
163 diagnostics,
164 snapshot: None,
165 };
166 }
167 };
168 let ast_for_snapshot = ast_with_prelude.clone();
169
170 let file_paths: FxHashMap<FileId, String> = files
174 .iter()
175 .map(|f| (f.file_id, f.path.to_string_lossy().into_owned()))
176 .collect();
177 let state = match compile_frontend_from_ast_with_file_paths(
178 ast_with_prelude,
179 interner_with_prelude,
180 preview_features,
181 true, target,
183 file_paths,
184 ) {
185 Ok(state) => state,
186 Err(errors) => {
187 for e in errors.iter() {
188 diagnostics.push(formatter.format_error(e));
189 }
190 return AnalysisResult {
195 diagnostics,
196 snapshot: build_ast_snapshot(files, preview_features).map(|s| s).ok(),
197 };
198 }
199 };
200
201 for warning in &state.warnings {
202 diagnostics.push(formatter.format_warning(warning));
203 }
204
205 let interner_for_snapshot = Arc::new(state.interner);
206 let type_pool = Arc::new(state.type_pool);
207
208 let mut expr_types: FxHashMap<Span, Type> = FxHashMap::default();
210 for f in &state.functions {
211 for (_air_ref, inst) in f.analyzed.air.iter() {
212 if inst.span.start == 0 && inst.span.end == 0 {
215 continue;
216 }
217 expr_types.entry(inst.span).or_insert(inst.ty);
222 }
223 }
224
225 AnalysisResult {
226 diagnostics,
227 snapshot: Some(build_snapshot_full(
228 files,
229 ast_for_snapshot,
230 interner_for_snapshot,
231 Some(type_pool),
232 expr_types,
233 )),
234 }
235}
236
237fn build_ast_snapshot(
241 files: &[WorkspaceFile],
242 preview_features: &PreviewFeatures,
243) -> Result<Snapshot, ()> {
244 let sources: Vec<SourceFile<'_>> = files
245 .iter()
246 .map(|f| SourceFile::new(path_str(&f.path), f.text.as_str(), f.file_id))
247 .collect();
248 let parsed = parse_all_files_with_preview(&sources, preview_features).map_err(|_| ())?;
249 let merged = merge_symbols(parsed).map_err(|_| ())?;
250 let (ast, interner) =
251 prepend_prelude(merged.ast, merged.interner, preview_features).map_err(|_| ())?;
252 let interner = Arc::new(interner);
253 Ok(build_snapshot(files, ast, interner))
254}
255
256fn build_snapshot(
257 files: &[WorkspaceFile],
258 ast: Ast,
259 interner: Arc<lasso::ThreadedRodeo>,
260) -> Snapshot {
261 build_snapshot_full(files, ast, interner, None, FxHashMap::default())
262}
263
264fn build_snapshot_full(
265 files: &[WorkspaceFile],
266 ast: Ast,
267 interner: Arc<lasso::ThreadedRodeo>,
268 type_pool: Option<Arc<TypeInternPool>>,
269 expr_types: FxHashMap<Span, Type>,
270) -> Snapshot {
271 let mut sources: FxHashMap<FileId, WorkspaceFile> = FxHashMap::default();
272 let mut path_to_file_id: FxHashMap<PathBuf, FileId> = FxHashMap::default();
273 let mut line_maps: FxHashMap<FileId, LineMap> = FxHashMap::default();
274 for f in files {
275 line_maps.insert(f.file_id, LineMap::new(&f.text));
276 path_to_file_id.insert(f.path.clone(), f.file_id);
277 sources.insert(f.file_id, f.clone());
278 }
279 Snapshot {
280 ast,
281 interner,
282 sources,
283 path_to_file_id,
284 line_maps,
285 type_pool,
286 expr_types,
287 }
288}
289
290fn path_str(path: &std::path::Path) -> &str {
291 path.to_str().unwrap_or("<non-utf8>")
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 fn wsf(path: &str, text: &str, id: u32) -> WorkspaceFile {
299 WorkspaceFile {
300 path: PathBuf::from(path),
301 text: text.to_string(),
302 file_id: FileId::new(id),
303 }
304 }
305
306 #[test]
307 fn compiles_clean_program() {
308 let files = vec![wsf("main.gruel", "fn main() -> i32 { 0 }", 1)];
309 let res = analyze(&files, &PreviewFeatures::default(), &Target::host());
310 assert!(
311 res.diagnostics.is_empty(),
312 "expected no diagnostics, got: {:?}",
313 res.diagnostics
314 );
315 assert!(res.snapshot.is_some());
316 }
317
318 #[test]
319 fn reports_type_error() {
320 let files = vec![wsf("main.gruel", "fn main() -> i32 { true }", 1)];
321 let res = analyze(&files, &PreviewFeatures::default(), &Target::host());
322 assert!(
323 !res.diagnostics.is_empty(),
324 "expected diagnostics for type error"
325 );
326 assert!(res.diagnostics.iter().any(|d| d.severity == "error"));
327 }
328
329 #[test]
330 fn reports_warnings() {
331 let files = vec![wsf("main.gruel", "fn main() -> i32 { let x = 42; 0 }", 1)];
332 let res = analyze(&files, &PreviewFeatures::default(), &Target::host());
333 assert!(res.diagnostics.iter().any(|d| d.severity == "warning"));
335 }
336}