Skip to main content

dada_compiler/
lib.rs

1use std::{
2    str::FromStr,
3    sync::{Arc, Mutex, mpsc::Sender},
4};
5
6use dada_ir_ast::{
7    DebugEvent,
8    ast::{AstFunction, AstItem, AstMember, Identifier},
9    diagnostic::Diagnostic,
10    inputs::{CompilationRoot, Krate, SourceFile},
11    span::AbsoluteSpan,
12};
13use dada_util::{Fallible, FromImpls, Map, Set, bail, debug};
14use salsa::{Database as _, Durability, Event, EventKind, Setter};
15use url::Url;
16
17mod fork;
18pub use fork::Fork;
19mod realfs;
20pub use realfs::RealFs;
21mod vfs;
22pub use vfs::VirtualFileSystem;
23use vfs::{ToUrl, UrlPath};
24
25use dada_parser::prelude::*;
26
27#[salsa::db]
28#[derive(Clone)]
29pub struct Compiler {
30    storage: salsa::Storage<Self>,
31
32    /// Extra information about our inputs that the rest of Dada compiler doesn't have to know
33    /// (e.g., they're URL etc).
34    ///
35    /// This is behind a mutex but we have an invariant that we only modify things in the mutex
36    /// if we have `&mut` access to the compiler.
37    inputs: Arc<Mutex<Inputs>>,
38
39    /// Mediates all access to the file system.
40    vfs: Arc<dyn VirtualFileSystem>,
41
42    /// Directory where debug logs are written.
43    debug_tx: Option<Sender<DebugEvent>>,
44}
45
46impl Compiler {
47    pub fn new(vfs: impl VirtualFileSystem, debug_tx: Option<Sender<DebugEvent>>) -> Self {
48        Self {
49            storage: Default::default(),
50            inputs: Default::default(),
51            vfs: Arc::new(vfs),
52            debug_tx,
53        }
54    }
55
56    /// Create a "fork" of the compiler that has only `&self` access.
57    /// This is meant to be used from another thread.
58    pub fn fork(&self) -> Fork<Self> {
59        Fork::from(Self {
60            storage: self.storage.clone(),
61            inputs: self.inputs.clone(),
62            vfs: self.vfs.clone(),
63            debug_tx: self.debug_tx.clone(),
64        })
65    }
66
67    /// Load the contents of `source_url` and then open it with those contents.
68    pub fn load_source_file(&mut self, source_url: &(impl ToUrl + ?Sized)) -> Fallible<SourceFile> {
69        let source_url = &source_url.to_url(&*self.vfs)?;
70        let contents = match self.vfs.contents(source_url) {
71            Ok(s) => Ok(s),
72            Err(e) => Err(e.to_string()),
73        };
74
75        self.open_source_file(source_url, contents)
76    }
77
78    /// "Open" a source file with the given contents.
79    /// This will find an existing `SourceFile` if one exists and update its content.
80    /// If none exists, a new `SourceFile` will be created and the containing crate will be added.
81    pub fn open_source_file(
82        &mut self,
83        source_url: &(impl ToUrl + ?Sized),
84        contents: Result<String, String>,
85    ) -> Fallible<SourceFile> {
86        let source_url = &source_url.to_url(&*self.vfs)?;
87        let source_file = match self.get_source_file(source_url) {
88            Some(v) => v,
89            None => {
90                self.add_crate_containing_source_file(source_url)?;
91                self.get_or_create_source_file(source_url)
92            }
93        };
94
95        let _ = source_file.set_contents(self).to(contents);
96        Ok(source_file)
97    }
98
99    /// Get the `SourceFile` for the given path.
100    /// Errors if no source file was opened yet.
101    pub fn get_previously_opened_source_file(
102        &mut self,
103        source_url: &(impl ToUrl + ?Sized),
104    ) -> Fallible<SourceFile> {
105        let source_url = source_url.to_url(&*self.vfs)?;
106        match self.get_source_file(&source_url) {
107            Some(v) => Ok(v),
108            None => {
109                bail!("no source file `{source_url}`")
110            }
111        }
112    }
113
114    /// Given a .dada file, finds the enclosing crate and adds it into the list of crates.
115    /// Given some path `a/b/c.dada`, we decide that `c` is a submodule of `a/b` if there exists
116    /// a `a/b.dada`; otherwise, `c` is considered a crate of its own.
117    pub fn add_crate_containing_source_file(&mut self, source_url: &Url) -> Fallible<Krate> {
118        let url_path = UrlPath::from(source_url.clone());
119
120        if !url_path.is_dada_file() {
121            bail!("source URL not a `.dada` file: `{source_url}`");
122        }
123
124        // We are at `a/b/c.dada`. If there exists a path `a/b.dada`, then `c` is a submodule of `a.b`.
125        // Otherwise, `c` is the root.
126        let mut krate_path = url_path.clone();
127        while !krate_path.is_empty() {
128            krate_path = krate_path.pop();
129
130            if !self.vfs.exists(&krate_path.dada_url()) {
131                break;
132            }
133        }
134
135        self.add_crate_with_root_path(&krate_path.dada_url())
136    }
137
138    /// Add a crate that is rooted in the given `dada` file.
139    /// The crate is named after the file name.
140    pub fn add_crate_with_root_path(&mut self, root_url: &Url) -> Fallible<Krate> {
141        let url_path = UrlPath::from(root_url.clone());
142
143        if !url_path.is_dada_file() {
144            bail!("crate root path should have `.dada` extension: `{root_url}`");
145        }
146
147        let crate_name = url_path.final_module_name().to_string();
148
149        // For a given crate, the root module would be called
150        // `foo.dada` and then any submodules will be in
151        // `foo/...`.
152        let root_dir_path = url_path.make_directory();
153
154        self.add_crate(crate_name.to_string(), root_dir_path.url())
155    }
156
157    /// Codegen the main function of a source file.
158    pub fn codegen_main_fn(&self, source_file: SourceFile) -> &Option<Vec<u8>> {
159        dada_codegen::codegen_main_fn(self, source_file)
160    }
161
162    /// Compute all diagnostics for a source file.
163    pub fn check_all(&self, source_file: SourceFile) -> Vec<&Diagnostic> {
164        Self::deduplicated(check_all::accumulated::<Diagnostic>(self, source_file))
165    }
166
167    /// Return type of the variable found at the given `span` or `None` if there is no variable there.
168    pub fn probe_variable_type(&self, span: AbsoluteSpan) -> Option<String> {
169        self.attach(|db| dada_probe::probe_variable_type(db, span))
170    }
171
172    /// Return type of the variable found at the given `span` or `None` if there is no variable there.
173    pub fn probe_expression_type(&self, span: AbsoluteSpan) -> Option<String> {
174        self.attach(|db| dada_probe::probe_expression_type(db, span))
175    }
176
177    /// Return compact AST representation of the expression at the given `span`.
178    pub fn probe_ast(&self, span: AbsoluteSpan) -> Option<String> {
179        self.attach(|db| dada_probe::probe_ast(db, span))
180    }
181
182    fn deduplicated(mut diagnostics: Vec<&Diagnostic>) -> Vec<&Diagnostic> {
183        let mut new = Set::default();
184        diagnostics.retain(|&d| new.insert(d));
185        diagnostics
186    }
187
188    pub fn fn_asts(&self, source_file: SourceFile) -> String {
189        use std::fmt::Write;
190
191        let mut output = String::new();
192
193        self.attach(|_db| {
194            let source = source_file.url_display(self);
195            writeln!(output, "# fn parse tree from {source}").unwrap();
196            writeln!(output).unwrap();
197
198            writeln!(output, "{}", fn_asts(self, source_file)).unwrap();
199        });
200
201        output
202    }
203
204    /// Access the [`CompilationRoot`], from which all crates and sources can be reached.
205    pub fn root(&self) -> CompilationRoot {
206        let mut inputs = self.inputs.lock().unwrap();
207        if let Some(root) = inputs.root {
208            return root;
209        }
210
211        // For now, just load libdada from the directory in the source tree
212        let libdada = Krate::new(self, "dada".to_string());
213        inputs.directories.insert(libdada, KrateSource::Libdada);
214
215        let root = CompilationRoot::new(self, vec![libdada]);
216        inputs.root = Some(root);
217        root
218    }
219
220    /// Add a crate named `crate_name` sourced at `source` into our list.
221    ///
222    /// We can never have two crates with the same name.
223    /// If a crate `k` named `crate_name` already exists, we check if `k` has the same `source`.
224    /// If so, the existing crate is returned. Otherwise, an error results.
225    fn add_crate(
226        &mut self,
227        crate_name: String,
228        new_source: impl Into<KrateSource>,
229    ) -> Fallible<Krate> {
230        let new_source: KrateSource = new_source.into();
231        let root = self.root();
232        let mut crates = root.crates(self).clone();
233
234        if let Some(&krate) = crates.iter().find(|c| *c.name(self) == crate_name) {
235            let inputs = self.inputs.lock().unwrap();
236            let krate_source = inputs.directories.get(&krate).unwrap();
237            if *krate_source == new_source {
238                return Ok(krate);
239            }
240            bail!("crate `{crate_name}` already exists: {krate_source}");
241        }
242
243        let krate = Krate::new(self, crate_name);
244
245        self.inputs
246            .lock()
247            .unwrap()
248            .directories
249            .insert(krate, new_source);
250
251        crates.push(krate);
252        root.set_crates(self)
253            .with_durability(Durability::HIGH)
254            .to(crates);
255
256        Ok(krate)
257    }
258
259    /// If there is a source file registered at `path`, return it.
260    /// Else return `None`.
261    fn get_source_file(&self, url: &Url) -> Option<SourceFile> {
262        self.inputs.lock().unwrap().source_files.get(url).copied()
263    }
264
265    /// Get or create a source-file at a given path.
266    fn get_or_create_source_file(&self, url: &Url) -> SourceFile {
267        let mut inputs = self.inputs.lock().unwrap();
268
269        if let Some(&opt_source_file) = inputs.source_files.get(url) {
270            return opt_source_file;
271        }
272
273        let contents = match self.vfs.contents(url) {
274            Ok(data) => Ok(data),
275            Err(e) => Err(format!("error reading `{url}`: {e}")),
276        };
277
278        let result = SourceFile::new(self, url.clone(), contents);
279
280        inputs.source_files.insert(url.clone(), result);
281
282        result
283    }
284}
285
286#[salsa::db]
287pub trait Db: dada_check::Db {}
288
289#[salsa::db]
290impl salsa::Database for Compiler {
291    fn salsa_event(&self, event: &dyn Fn() -> Event) {
292        if dada_util::log::is_enabled() {
293            let event = event();
294            match event.kind {
295                EventKind::DidValidateMemoizedValue { database_key } => {
296                    debug!("did_validate_memoized_value", database_key);
297                }
298                EventKind::WillBlockOn {
299                    other_thread_id,
300                    database_key,
301                } => {
302                    debug!("will_block_on", other_thread_id, database_key);
303                }
304                EventKind::WillExecute { database_key } => {
305                    debug!("will_execute", database_key);
306                }
307                EventKind::DidSetCancellationFlag => {
308                    debug!("did_set_cancellation_flag");
309                }
310                EventKind::WillCheckCancellation
311                | EventKind::WillDiscardStaleOutput { .. }
312                | EventKind::DidDiscard { .. }
313                | EventKind::DidDiscardAccumulated { .. }
314                | EventKind::DidInternValue { .. }
315                | EventKind::DidReinternValue { .. } => {}
316            }
317        }
318    }
319}
320
321#[salsa::db]
322impl dada_ir_ast::Db for Compiler {
323    fn url_display(&self, url: &Url) -> String {
324        self.vfs.url_display(url)
325    }
326
327    fn root(&self) -> CompilationRoot {
328        Compiler::root(self)
329    }
330
331    fn source_file<'db>(&'db self, krate: Krate, modules: &[Identifier<'db>]) -> SourceFile {
332        let source = self.inputs.lock().unwrap().directories[&krate].clone();
333        match source {
334            KrateSource::Url(url) => {
335                let mut url_path = UrlPath::from(url);
336                assert!(!url_path.is_dada_file());
337                for module in modules {
338                    url_path.push(module.text(self));
339                }
340                self.get_or_create_source_file(&url_path.make_dada_file().url())
341            }
342
343            KrateSource::Libdada => {
344                let mut path = String::new();
345                for module in modules {
346                    path.push('/');
347                    path.push_str(module.text(self));
348                }
349                path.push_str(".dada");
350
351                if let Some(libdada_source) =
352                    self.inputs.lock().unwrap().libdada_source_files.get(&path)
353                {
354                    return *libdada_source;
355                }
356
357                let contents = match LibDadaAsset::get(&path[1..]) {
358                    Some(embedded) => match String::from_utf8(embedded.data.into_owned()) {
359                        Ok(data) => Ok(data),
360                        Err(e) => Err(format!("libdada file `{path}` is not utf-8: {e}")),
361                    },
362                    None => Err(format!("no libdada module at `{path}`")),
363                };
364
365                let libdada_url = Url::from_str(format!("libdada:///{path}").as_str()).unwrap();
366                let result = SourceFile::new(self, libdada_url, contents);
367                self.inputs
368                    .lock()
369                    .unwrap()
370                    .libdada_source_files
371                    .insert(path, result);
372
373                result
374            }
375        }
376    }
377
378    fn debug_tx(&self) -> Option<Sender<DebugEvent>> {
379        self.debug_tx.clone()
380    }
381}
382
383#[salsa::db]
384impl Db for Compiler {}
385
386#[derive(rust_embed::Embed)]
387#[folder = "../../libdada"]
388struct LibDadaAsset;
389
390#[derive(Default)]
391struct Inputs {
392    root: Option<CompilationRoot>,
393    source_files: Map<Url, SourceFile>,
394    libdada_source_files: Map<String, SourceFile>,
395    directories: Map<Krate, KrateSource>,
396}
397
398#[derive(FromImpls, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
399pub enum KrateSource {
400    Url(Url),
401
402    #[no_from_impl]
403    Libdada,
404}
405
406impl std::fmt::Display for KrateSource {
407    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
408        match self {
409            Self::Url(url) => write!(f, "rooted at `{url}`"),
410            Self::Libdada => write!(f, "built-in libdada"),
411        }
412    }
413}
414
415#[salsa::tracked]
416fn check_all(db: &dyn Db, source_file: SourceFile) {
417    use dada_check::Check;
418    source_file.check(db);
419}
420
421fn fn_asts(db: &dyn Db, source_file: SourceFile) -> String {
422    use std::fmt::Write;
423
424    let mut output = String::new();
425
426    let module = source_file.parse(db);
427
428    for item in module.items(db) {
429        match *item {
430            AstItem::SourceFile(_source_file) => (),
431            AstItem::Use(_use_item) => (),
432            AstItem::Aggregate(class_item) => {
433                writeln!(output, "## class `{}`", class_item.name(db)).unwrap();
434                for member in class_item.members(db) {
435                    match member {
436                        AstMember::Field(_field_decl) => (),
437                        AstMember::Function(function) => {
438                            writeln!(output, "### fn `{}`", function.name(db).id).unwrap();
439                            writeln!(output).unwrap();
440                            writeln!(output, "{}", fn_asts_fn(db, *function)).unwrap();
441                        }
442                    }
443                }
444            }
445            AstItem::Function(function) => {
446                writeln!(output, "## fn `{}`", function.name(db).id).unwrap();
447                writeln!(output).unwrap();
448                writeln!(output, "{}", fn_asts_fn(db, function)).unwrap();
449            }
450            AstItem::MainFunction(ast_main_function) => {
451                writeln!(output, "## main fn").unwrap();
452                writeln!(output).unwrap();
453                for statement in ast_main_function.statements(db) {
454                    writeln!(output, "{statement:?}").unwrap();
455                }
456            }
457        }
458    }
459
460    return output;
461
462    fn fn_asts_fn<'db>(db: &'db dyn Db, function: AstFunction<'db>) -> String {
463        if let Some(block) = function.body_block(db) {
464            format!("{block:#?}")
465        } else {
466            "None".to_string()
467        }
468    }
469}