Skip to main content

lutra_compiler/
check.rs

1use std::path::{Path, PathBuf};
2use std::sync::{Arc, OnceLock};
3
4use itertools::Itertools;
5
6use crate::diagnostic::{Diagnostic, WithErrorInfo};
7use crate::error::Error;
8use crate::pr;
9use crate::project::SourceOverlay;
10use crate::project::SourceProvider;
11use crate::project::{self, Dependency};
12use crate::resolver::NS_STD;
13use crate::{SourceTree, diagnostic};
14use crate::{Span, error};
15
16#[cfg_attr(feature = "clap", derive(clap::Parser))]
17#[derive(Default, Clone)]
18pub struct CheckParams {
19    #[cfg_attr(feature = "clap", arg(skip))]
20    pub dependencies: Vec<Dependency>,
21}
22
23pub fn check(
24    mut source: project::SourceTree,
25    params: CheckParams,
26) -> Result<project::Project, error::Error> {
27    // parse
28    if source.is_empty() {
29        source.insert(PathBuf::from(""), "".into());
30    }
31    let root_mod = parse(&source)?.map_err(|e| Error::from_diagnostics(e, &source))?;
32
33    // resolve
34    let metadata = root_mod.get_anno_at(&pr::Path::empty(), pr::Anno::as_std_metadata);
35    let is_std = metadata.is_some_and(|m| m == NS_STD);
36    let mut dependencies = params.dependencies;
37    if !is_std {
38        dependencies.push(Dependency {
39            name: NS_STD.into(),
40            inner: std_project()?,
41        });
42    }
43    let mut project = crate::resolver::resolve(root_mod, dependencies, is_std)
44        .map_err(|e| Error::from_diagnostics(e, &source))?;
45    project.source = source;
46    Ok(project)
47}
48
49pub fn check_overlay(
50    project: &project::Project,
51    overlay: &str,
52    overlay_name: Option<&str>,
53) -> Result<pr::Expr, error::Error> {
54    let source = crate::project::SourceOverlay::new(&project.source, overlay, overlay_name);
55
56    parse_overlay(&source)
57        .and_then(|expr| crate::resolver::resolve_overlay_expr(&project.root_module, expr))
58        .map_err(|e| Error::from_diagnostics(e, &source))
59}
60
61fn parse(tree: &SourceTree) -> Result<Result<pr::ModuleDef, Vec<Diagnostic>>, error::Error> {
62    // init the root module def
63    let mut root = pr::ModuleDef::default();
64
65    // parse and insert into the root
66    let mut diags = Vec::new();
67    for source_id in tree.get_ids() {
68        let (path, content) = tree.get_by_id(source_id).unwrap();
69
70        let module_path = os_path_to_mod_path(path)?;
71
72        let (parsed, errs, _) = crate::parser::parse_source(content, source_id);
73        diags.extend(errs);
74        if let Some(parsed) = parsed {
75            // TODO: improve these error messages
76
77            if module_path.is_empty() && parsed.is_submodule {
78                diags.push(
79                    Diagnostic::new_custom("cannot load the project root")
80                        .with_span(Some(Span {
81                            start: 0,
82                            len: 1,
83                            source_id,
84                        }))
85                        .push_hint(format!("file {} is a submodule", path.display())),
86                );
87            }
88            let included = module_path.is_empty() || parsed.is_submodule;
89            if included {
90                diags.extend(insert_module_at_path(&mut root, module_path, parsed.root));
91            }
92        }
93    }
94    Ok(if diags.is_empty() {
95        Ok(root)
96    } else {
97        Err(diags)
98    })
99}
100
101fn parse_overlay(overlay: &SourceOverlay) -> Result<pr::Expr, Vec<Diagnostic>> {
102    let id = SourceOverlay::overlay_id();
103    let (_path, content) = overlay.get_by_id(id).unwrap();
104    let (ast, diagnostics) = crate::parser::parse_expr(content, id);
105    if diagnostics.is_empty() {
106        Ok(ast.unwrap())
107    } else {
108        Err(diagnostics)
109    }
110}
111
112pub fn insert_module_at_path(
113    module: &mut pr::ModuleDef,
114    mut path: Vec<String>,
115    to_insert: pr::ModuleDef,
116) -> Vec<diagnostic::Diagnostic> {
117    if path.is_empty() {
118        let mut d = Vec::new();
119
120        module.annotations.extend(to_insert.annotations);
121        module.span_content = to_insert.span_content;
122        module.imports.extend(to_insert.imports);
123        for (name, def) in to_insert.defs {
124            let conflict = module.defs.insert(name, def);
125            if let Some(conflict) = conflict {
126                d.push(
127                    diagnostic::Diagnostic::new_custom("duplicate name").with_span(conflict.span),
128                );
129            }
130        }
131        return d;
132    }
133
134    let step = path.remove(0);
135
136    // find submodule def
137    let submodule = module
138        .defs
139        .entry(step)
140        .or_insert_with(|| pr::Def::new(pr::ModuleDef::default()));
141    let pr::DefKind::Module(submodule) = &mut submodule.kind else {
142        return vec![
143            diagnostic::Diagnostic::new_custom("duplicate name").with_span(submodule.span),
144        ];
145    };
146    insert_module_at_path(submodule, path, to_insert)
147}
148
149fn os_path_to_mod_path(path: &Path) -> Result<Vec<String>, error::Error> {
150    let path = if path.ends_with("module.lt") {
151        // remove module.lt suffix
152        path.parent().unwrap().to_path_buf()
153    } else {
154        // remove file format extension
155        path.with_extension("")
156    };
157
158    // split by /
159    path.components()
160        .map(|x| {
161            x.as_os_str()
162                .to_str()
163                .map(str::to_string)
164                .ok_or_else(|| error::Error::InvalidPath { path: path.clone() })
165        })
166        .try_collect()
167}
168
169pub fn std_source() -> SourceTree {
170    SourceTree::single(
171        std::path::PathBuf::new(),
172        include_str!("std.lt").to_string(),
173    )
174}
175
176/// Compile `std.lt` once and cache the result for the lifetime of the process.
177static STD_PROJECT: OnceLock<Arc<crate::Project>> = OnceLock::new();
178
179fn std_project() -> Result<Arc<crate::Project>, error::Error> {
180    if let Some(cached) = STD_PROJECT.get() {
181        return Ok(Arc::clone(cached));
182    }
183
184    let project = Arc::new(check(std_source(), CheckParams::new())?);
185    let _ = STD_PROJECT.set(project);
186    Ok(Arc::clone(STD_PROJECT.get().unwrap()))
187}
188
189impl CheckParams {
190    pub fn new() -> Self {
191        Self::default()
192    }
193
194    pub fn with_dep(mut self, name: impl Into<String>, project: Arc<crate::Project>) -> Self {
195        self.dependencies.push(crate::project::Dependency {
196            name: name.into(),
197            inner: project,
198        });
199        self
200    }
201}