sixtyfps_compilerlib/
typeloader.rs

1// Copyright © SixtyFPS GmbH <info@sixtyfps.io>
2// SPDX-License-Identifier: (GPL-3.0-only OR LicenseRef-SixtyFPS-commercial)
3
4use std::borrow::Cow;
5use std::cell::RefCell;
6use std::collections::{BTreeMap, HashMap, HashSet};
7use std::path::{Path, PathBuf};
8use std::rc::Rc;
9
10use crate::diagnostics::{BuildDiagnostics, Spanned};
11use crate::object_tree::{self, Document};
12use crate::parser;
13use crate::parser::{syntax_nodes, NodeOrToken, SyntaxKind, SyntaxToken};
14use crate::typeregister::TypeRegister;
15use crate::CompilerConfiguration;
16
17/// Storage for a cache of all loaded documents
18#[derive(Default)]
19pub struct LoadedDocuments {
20    /// maps from the canonical file name to the object_tree::Document
21    docs: HashMap<PathBuf, Document>,
22    currently_loading: HashSet<PathBuf>,
23}
24
25pub struct ImportedTypes {
26    pub import_token: SyntaxToken,
27    pub imported_types: syntax_nodes::ImportSpecifier,
28    pub file: String,
29}
30
31#[derive(Debug)]
32pub struct ImportedName {
33    // name of export to match in the other file
34    pub external_name: String,
35    // name to be used locally
36    pub internal_name: String,
37}
38
39impl ImportedName {
40    pub fn extract_imported_names(
41        import: &syntax_nodes::ImportSpecifier,
42    ) -> Option<impl Iterator<Item = ImportedName>> {
43        import
44            .ImportIdentifierList()
45            .map(|import_identifiers| import_identifiers.ImportIdentifier().map(Self::from_node))
46    }
47
48    pub fn from_node(importident: syntax_nodes::ImportIdentifier) -> Self {
49        let external_name =
50            parser::normalize_identifier(importident.ExternalName().text().to_string().trim());
51
52        let internal_name = match importident.InternalName() {
53            Some(name_ident) => parser::normalize_identifier(name_ident.text().to_string().trim()),
54            None => external_name.clone(),
55        };
56
57        ImportedName { internal_name, external_name }
58    }
59}
60
61pub struct TypeLoader<'a> {
62    pub global_type_registry: Rc<RefCell<TypeRegister>>,
63    pub compiler_config: &'a CompilerConfiguration,
64    style: Cow<'a, str>,
65    all_documents: LoadedDocuments,
66}
67
68impl<'a> TypeLoader<'a> {
69    pub fn new(
70        global_type_registry: Rc<RefCell<TypeRegister>>,
71        compiler_config: &'a CompilerConfiguration,
72        diag: &mut BuildDiagnostics,
73    ) -> Self {
74        let style = compiler_config
75        .style
76        .as_ref()
77        .map(Cow::from)
78        .or_else(|| std::env::var("SIXTYFPS_STYLE").map(Cow::from).ok())
79        .unwrap_or_else(|| {
80            let is_wasm = cfg!(target_arch = "wasm32")
81                || std::env::var("TARGET").map_or(false, |t| t.starts_with("wasm"));
82            if !is_wasm {
83                diag.push_diagnostic_with_span("SIXTYFPS_STYLE not defined, defaulting to 'fluent', see https://github.com/sixtyfpsui/sixtyfps/issues/83 for more info".to_owned(),
84                    Default::default(),
85                    crate::diagnostics::DiagnosticLevel::Warning
86                );
87            }
88            Cow::from("fluent")
89        });
90
91        Self { global_type_registry, compiler_config, style, all_documents: Default::default() }
92    }
93
94    /// Imports of files that don't have the .60 extension are returned.
95    pub async fn load_dependencies_recursively(
96        &mut self,
97        doc: &syntax_nodes::Document,
98        diagnostics: &mut BuildDiagnostics,
99        registry_to_populate: &Rc<RefCell<TypeRegister>>,
100    ) -> Vec<ImportedTypes> {
101        let dependencies = self.collect_dependencies(doc, diagnostics).await;
102        let mut foreign_imports = vec![];
103        for mut import in dependencies {
104            if import.file.ends_with(".60") {
105                if let Some(imported_types) =
106                    ImportedName::extract_imported_names(&import.imported_types)
107                {
108                    self.load_dependency(import, imported_types, registry_to_populate, diagnostics)
109                        .await;
110                } else {
111                    diagnostics.push_error(
112                    "Import names are missing. Please specify which types you would like to import"
113                        .into(),
114                    &import.import_token,
115                );
116                }
117            } else {
118                import.file = self
119                    .resolve_import_path(Some(&import.import_token.clone().into()), &import.file)
120                    .0
121                    .to_string_lossy()
122                    .to_string();
123                foreign_imports.push(import);
124            }
125        }
126        foreign_imports
127    }
128
129    pub async fn import_type(
130        &mut self,
131        file_to_import: &str,
132        type_name: &str,
133        diagnostics: &mut BuildDiagnostics,
134    ) -> Option<crate::langtype::Type> {
135        let doc_path = match self.ensure_document_loaded(file_to_import, None, diagnostics).await {
136            Some(doc_path) => doc_path,
137            None => return None,
138        };
139
140        let doc = self.all_documents.docs.get(&doc_path).unwrap();
141
142        doc.exports().iter().find_map(|(export_name, ty)| {
143            if type_name == export_name.as_str() {
144                Some(ty.clone())
145            } else {
146                None
147            }
148        })
149    }
150
151    /// Append a possibly relative path to a base path. Returns the data if it resolves to a built-in (compiled-in)
152    /// file.
153    pub fn resolve_import_path(
154        &self,
155        import_token: Option<&NodeOrToken>,
156        maybe_relative_path_or_url: &str,
157    ) -> (std::path::PathBuf, Option<&'static [u8]>) {
158        let referencing_file_or_url =
159            import_token.and_then(|tok| tok.source_file().map(|s| s.path()));
160
161        self.find_file_in_include_path(referencing_file_or_url, maybe_relative_path_or_url)
162            .unwrap_or_else(|| {
163                (
164                    referencing_file_or_url
165                        .and_then(|base_path_or_url| {
166                            let base_path_or_url_str = base_path_or_url.to_string_lossy();
167                            if base_path_or_url_str.contains("://") {
168                                url::Url::parse(&base_path_or_url_str).ok().and_then(|base_url| {
169                                    base_url
170                                        .join(maybe_relative_path_or_url)
171                                        .ok()
172                                        .map(|url| url.to_string().into())
173                                })
174                            } else {
175                                base_path_or_url.parent().and_then(|base_dir| {
176                                    dunce::canonicalize(base_dir.join(maybe_relative_path_or_url))
177                                        .ok()
178                                })
179                            }
180                        })
181                        .unwrap_or_else(|| maybe_relative_path_or_url.into()),
182                    None,
183                )
184            })
185    }
186
187    async fn ensure_document_loaded<'b>(
188        &'b mut self,
189        file_to_import: &'b str,
190        import_token: Option<NodeOrToken>,
191        diagnostics: &'b mut BuildDiagnostics,
192    ) -> Option<PathBuf> {
193        let (path, is_builtin) = self.resolve_import_path(import_token.as_ref(), file_to_import);
194
195        let path_canon = dunce::canonicalize(&path).unwrap_or_else(|_| path.to_owned());
196
197        if self.all_documents.docs.get(path_canon.as_path()).is_some() {
198            return Some(path_canon);
199        }
200
201        // Drop &self lifetime attached to is_builtin, in order to mutable borrow self below
202        let builtin = is_builtin.map(|s| s.to_owned());
203        let is_builtin = builtin.is_some();
204
205        if !self.all_documents.currently_loading.insert(path_canon.clone()) {
206            diagnostics
207                .push_error(format!("Recursive import of \"{}\"", path.display()), &import_token);
208            return None;
209        }
210
211        let source_code_result = if let Some(builtin) = builtin {
212            Ok(String::from_utf8(builtin)
213                .expect("internal error: embedded file is not UTF-8 source code"))
214        } else if let Some(fallback) = &self.compiler_config.open_import_fallback {
215            let result = fallback(path_canon.to_string_lossy().into()).await;
216            result.unwrap_or_else(|| std::fs::read_to_string(&path_canon))
217        } else {
218            std::fs::read_to_string(&path_canon)
219        };
220
221        let source_code = match source_code_result {
222            Ok(source) => source,
223            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
224                diagnostics.push_error(
225                    format!(
226                        "Cannot find requested import \"{}\" in the include search path",
227                        file_to_import
228                    ),
229                    &import_token,
230                );
231                return None;
232            }
233            Err(err) => {
234                diagnostics.push_error(
235                    format!("Error reading requested import \"{}\": {}", path.display(), err),
236                    &import_token,
237                );
238                return None;
239            }
240        };
241
242        self.load_file(&path_canon, &path, source_code, is_builtin, diagnostics).await;
243        let _ok = self.all_documents.currently_loading.remove(path_canon.as_path());
244        assert!(_ok);
245        Some(path_canon)
246    }
247
248    /// Load a file, and its dependency not run the passes.
249    ///
250    /// the path must be the canonical path
251    pub async fn load_file(
252        &mut self,
253        path: &Path,
254        source_path: &Path,
255        source_code: String,
256        is_builtin: bool,
257        diagnostics: &mut BuildDiagnostics,
258    ) {
259        let dependency_doc: syntax_nodes::Document =
260            crate::parser::parse(source_code, Some(source_path), diagnostics).into();
261
262        let dependency_registry =
263            Rc::new(RefCell::new(TypeRegister::new(&self.global_type_registry)));
264        dependency_registry.borrow_mut().expose_internal_types = is_builtin;
265        let foreign_imports = self
266            .load_dependencies_recursively(&dependency_doc, diagnostics, &dependency_registry)
267            .await;
268
269        if diagnostics.has_error() {
270            // If there was error (esp parse error) we don't want to report further error in this document.
271            // because they might be nonsense (TODO: we should check that the parse error were really in this document).
272            // But we still want to create a document to give better error messages in the root document.
273            let mut ignore_diag = BuildDiagnostics::default();
274            ignore_diag.push_error_with_span(
275                "Dummy error because some of the code asserts there was an error".into(),
276                Default::default(),
277            );
278            let doc = crate::object_tree::Document::from_node(
279                dependency_doc,
280                foreign_imports,
281                &mut ignore_diag,
282                &dependency_registry,
283            );
284            self.all_documents.docs.insert(path.to_owned(), doc);
285            return;
286        }
287        let doc = crate::object_tree::Document::from_node(
288            dependency_doc,
289            foreign_imports,
290            diagnostics,
291            &dependency_registry,
292        );
293        crate::passes::run_import_passes(&doc, self, diagnostics);
294
295        self.all_documents.docs.insert(path.to_owned(), doc);
296    }
297
298    fn load_dependency<'b>(
299        &'b mut self,
300        import: ImportedTypes,
301        imported_types: impl Iterator<Item = ImportedName> + 'b,
302        registry_to_populate: &'b Rc<RefCell<TypeRegister>>,
303        build_diagnostics: &'b mut BuildDiagnostics,
304    ) -> core::pin::Pin<Box<dyn std::future::Future<Output = ()> + 'b>> {
305        Box::pin(async move {
306            let doc_path = match self
307                .ensure_document_loaded(
308                    &import.file,
309                    Some(import.import_token.clone().into()),
310                    build_diagnostics,
311                )
312                .await
313            {
314                Some(path) => path,
315                None => return,
316            };
317
318            let doc = self.all_documents.docs.get(&doc_path).unwrap();
319            let exports = doc.exports();
320
321            for import_name in imported_types {
322                let imported_type = exports.iter().find_map(|(export_name, ty)| {
323                    if import_name.external_name == export_name.as_str() {
324                        Some(ty.clone())
325                    } else {
326                        None
327                    }
328                });
329
330                let imported_type = match imported_type {
331                    Some(ty) => ty,
332                    None => {
333                        build_diagnostics.push_error(
334                            format!(
335                                "No exported type called '{}' found in \"{}\"",
336                                import_name.external_name, import.file
337                            ),
338                            &import.import_token,
339                        );
340                        continue;
341                    }
342                };
343
344                registry_to_populate
345                    .borrow_mut()
346                    .insert_type_with_name(imported_type, import_name.internal_name);
347            }
348        })
349    }
350
351    /// Lookup a filename and try to find the absolute filename based on the include path or
352    /// the current file directory
353    pub fn find_file_in_include_path(
354        &self,
355        referencing_file: Option<&std::path::Path>,
356        file_to_import: &str,
357    ) -> Option<(PathBuf, Option<&'static [u8]>)> {
358        // The directory of the current file is the first in the list of include directories.
359        let maybe_current_directory =
360            referencing_file.and_then(|path| path.parent()).map(|p| p.to_path_buf());
361        maybe_current_directory
362            .clone()
363            .into_iter()
364            .chain(self.compiler_config.include_paths.iter().map(PathBuf::as_path).map({
365                |include_path| {
366                    if include_path.is_relative() && maybe_current_directory.as_ref().is_some() {
367                        maybe_current_directory.as_ref().unwrap().join(include_path)
368                    } else {
369                        include_path.to_path_buf()
370                    }
371                }
372            }))
373            .chain(std::iter::once_with(|| format!("builtin:/{}", self.style).into()))
374            .find_map(|include_dir| {
375                let candidate = include_dir.join(file_to_import);
376                crate::fileaccess::load_file(&candidate)
377                    .map(|virtual_file| (candidate, virtual_file.builtin_contents))
378            })
379    }
380
381    async fn collect_dependencies(
382        &mut self,
383        doc: &syntax_nodes::Document,
384        doc_diagnostics: &mut BuildDiagnostics,
385    ) -> impl Iterator<Item = ImportedTypes> {
386        type DependenciesByFile = BTreeMap<String, ImportedTypes>;
387        let mut dependencies = DependenciesByFile::new();
388
389        for import in doc.ImportSpecifier() {
390            let import_uri = match import.child_token(SyntaxKind::StringLiteral) {
391                Some(import_uri) => import_uri,
392                None => {
393                    debug_assert!(doc_diagnostics.has_error());
394                    continue;
395                }
396            };
397            let path_to_import = import_uri.text().to_string();
398            let path_to_import = path_to_import.trim_matches('\"').to_string();
399            if path_to_import.is_empty() {
400                doc_diagnostics.push_error("Unexpected empty import url".to_owned(), &import_uri);
401                continue;
402            }
403
404            dependencies.entry(path_to_import.clone()).or_insert_with(|| ImportedTypes {
405                import_token: import_uri,
406                imported_types: import,
407                file: path_to_import,
408            });
409        }
410
411        dependencies.into_iter().map(|(_, value)| value)
412    }
413
414    /// Return a document if it was already loaded
415    pub fn get_document<'b>(&'b self, path: &Path) -> Option<&'b object_tree::Document> {
416        dunce::canonicalize(path).map_or_else(
417            |_| self.all_documents.docs.get(path),
418            |path| self.all_documents.docs.get(&path),
419        )
420    }
421
422    /// Return an iterator over all the loaded file path
423    pub fn all_files<'b>(&'b self) -> impl Iterator<Item = &PathBuf> + 'b {
424        self.all_documents.docs.keys()
425    }
426
427    /// Returns an iterator over all the loaded documents
428    pub fn all_documents(&self) -> impl Iterator<Item = &object_tree::Document> + '_ {
429        self.all_documents.docs.values()
430    }
431}
432
433#[test]
434fn test_dependency_loading() {
435    let test_source_path: std::path::PathBuf =
436        [env!("CARGO_MANIFEST_DIR"), "tests", "typeloader"].iter().collect();
437
438    let mut incdir = test_source_path.clone();
439    incdir.push("incpath");
440
441    let mut compiler_config =
442        CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
443    compiler_config.include_paths = vec![incdir];
444    compiler_config.style = Some("fluent".into());
445
446    let mut main_test_path = test_source_path;
447    main_test_path.push("dependency_test_main.60");
448
449    let mut test_diags = crate::diagnostics::BuildDiagnostics::default();
450    let doc_node = crate::parser::parse_file(main_test_path, &mut test_diags).unwrap();
451
452    let doc_node: syntax_nodes::Document = doc_node.into();
453
454    let global_registry = TypeRegister::builtin();
455
456    let registry = Rc::new(RefCell::new(TypeRegister::new(&global_registry)));
457
458    let mut build_diagnostics = BuildDiagnostics::default();
459
460    let mut loader = TypeLoader::new(global_registry, &compiler_config, &mut build_diagnostics);
461
462    spin_on::spin_on(loader.load_dependencies_recursively(
463        &doc_node,
464        &mut build_diagnostics,
465        &registry,
466    ));
467
468    assert!(!test_diags.has_error());
469    assert!(!build_diagnostics.has_error());
470}
471
472#[test]
473fn test_load_from_callback_ok() {
474    let ok = Rc::new(core::cell::Cell::new(false));
475    let ok_ = ok.clone();
476
477    let mut compiler_config =
478        CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
479    compiler_config.style = Some("fluent".into());
480    compiler_config.open_import_fallback = Some(Rc::new(move |path| {
481        let ok_ = ok_.clone();
482        Box::pin(async move {
483            assert_eq!(path, "../FooBar.60");
484            assert!(!ok_.get());
485            ok_.set(true);
486            Some(Ok("export XX := Rectangle {} ".to_owned()))
487        })
488    }));
489
490    let mut test_diags = crate::diagnostics::BuildDiagnostics::default();
491    let doc_node = crate::parser::parse(
492        r#"
493/* ... */
494import { XX } from "../FooBar.60";
495X := XX {}
496"#
497        .into(),
498        Some(std::path::Path::new("HELLO")),
499        &mut test_diags,
500    );
501
502    let doc_node: syntax_nodes::Document = doc_node.into();
503    let global_registry = TypeRegister::builtin();
504    let registry = Rc::new(RefCell::new(TypeRegister::new(&global_registry)));
505    let mut build_diagnostics = BuildDiagnostics::default();
506    let mut loader = TypeLoader::new(global_registry, &compiler_config, &mut build_diagnostics);
507    spin_on::spin_on(loader.load_dependencies_recursively(
508        &doc_node,
509        &mut build_diagnostics,
510        &registry,
511    ));
512    assert!(ok.get());
513    assert!(!test_diags.has_error());
514    assert!(!build_diagnostics.has_error());
515}
516
517#[test]
518fn test_manual_import() {
519    let mut compiler_config =
520        CompilerConfiguration::new(crate::generator::OutputFormat::Interpreter);
521    compiler_config.style = Some("fluent".into());
522    let global_registry = TypeRegister::builtin();
523    let mut build_diagnostics = BuildDiagnostics::default();
524    let mut loader = TypeLoader::new(global_registry, &compiler_config, &mut build_diagnostics);
525
526    let maybe_button_type = spin_on::spin_on(loader.import_type(
527        "sixtyfps_widgets.60",
528        "Button",
529        &mut build_diagnostics,
530    ));
531
532    assert!(!build_diagnostics.has_error());
533    assert!(maybe_button_type.is_some());
534}