Skip to main content

decl_lang/
module.rs

1//! Module loading, linking, and universe evaluation (module.ts, §8.1–8.5,
2//! §8.8): files are modules, the import graph is acyclic, exports are
3//! explicit; packages (§8.6–8.7) plug in through the resolver hook.
4use crate::ast::*;
5use crate::engine::{Engine, RootSrc};
6use crate::package::Resolver;
7use crate::parse::parse_source;
8use crate::semantics::*;
9use std::cell::RefCell;
10use std::collections::HashMap;
11use std::path::{Path, PathBuf};
12use std::rc::Rc;
13
14pub struct Module {
15    pub path: PathBuf,
16    pub decls: Vec<Decl>,
17    pub env: Rc<Env>,
18    pub exports: Rc<RefCell<HashMap<String, Export>>>,
19}
20
21pub struct LoadResult {
22    pub modules: Vec<Rc<Module>>,
23    pub entry: Option<Rc<Module>>,
24    pub diags: Vec<Diag>,
25}
26
27struct Loader {
28    modules: HashMap<PathBuf, Rc<Module>>,
29    order: Vec<Rc<Module>>,
30    visiting: Vec<PathBuf>,
31    diags: Vec<Diag>,
32    resolver: Option<Resolver>,
33    overrides: HashMap<PathBuf, String>,
34}
35
36impl Loader {
37    fn report(&mut self, code: &str, message: String) {
38        self.diags
39            .push(Diag::error(message, String::new(), Some(code)));
40    }
41    fn resolve_spec(&mut self, spec: &str, from_dir: &Path) -> Option<PathBuf> {
42        if spec.starts_with("./") || spec.starts_with("../") {
43            return Some(normalize(&from_dir.join(spec)));
44        }
45        let Some(resolver) = self.resolver.clone() else {
46            self.report(
47                "E3010",
48                format!("package import \"{spec}\" outside a package (no manifest)"),
49            );
50            return None;
51        };
52        match resolver(spec, from_dir) {
53            Ok(p) => Some(p),
54            Err((code, message)) => {
55                self.report(&code, message);
56                None
57            }
58        }
59    }
60    fn load(&mut self, path: &Path) -> Option<Rc<Module>> {
61        let abs = normalize(&std::path::absolute(path).unwrap_or_else(|_| path.to_path_buf()));
62        if let Some(m) = self.modules.get(&abs) {
63            return Some(m.clone());
64        }
65        if let Some(ci) = self.visiting.iter().position(|p| *p == abs) {
66            let cycle: Vec<String> = self.visiting[ci..]
67                .iter()
68                .chain(std::iter::once(&abs))
69                .map(|p| p.display().to_string())
70                .collect();
71            self.report(
72                "E3007",
73                format!("module import cycle: {}", cycle.join(" -> ")),
74            );
75            return None;
76        }
77        let src = match self.overrides.get(&abs) {
78            Some(s) => s.clone(),
79            None => match std::fs::read_to_string(&abs) {
80                Ok(s) => s,
81                Err(_) => {
82                    self.report("E3004", format!("module not found: {}", abs.display()));
83                    return None;
84                }
85            },
86        };
87        let parsed = parse_source(&src);
88        if !parsed.errors.is_empty() {
89            self.report(
90                "E2001",
91                format!("{}: {} parse error(s)", abs.display(), parsed.errors.len()),
92            );
93            return None;
94        }
95        let env = Env::new();
96        env.load(&parsed.decls);
97        for n in env.duplicates.borrow().iter() {
98            self.diags.push(Diag::error(
99                format!("duplicate name {n} in {}", abs.display()),
100                String::new(),
101                Some("E3001"),
102            ));
103        }
104        let m = Rc::new(Module {
105            path: abs.clone(),
106            decls: parsed.decls,
107            env,
108            exports: Rc::new(RefCell::new(HashMap::new())),
109        });
110        self.visiting.push(abs.clone());
111        let mut targets: HashMap<String, Rc<Module>> = HashMap::new();
112        let from_dir = abs.parent().map(|p| p.to_path_buf()).unwrap_or_default();
113        for d in &m.decls {
114            let from = match &d.body {
115                DeclBody::Import { from, .. } | DeclBody::ReExport { from, .. } => from.clone(),
116                _ => continue,
117            };
118            if let Some(t) = self.resolve_spec(&from, &from_dir) {
119                if let Some(tm) = self.load(&t) {
120                    targets.insert(from, tm);
121                }
122            }
123        }
124        self.visiting.pop();
125        self.modules.insert(abs.clone(), m.clone());
126
127        let taken = |n: &str| {
128            let e = &m.env;
129            e.type_asts.borrow().contains_key(n)
130                || e.consts.borrow().contains_key(n)
131                || e.funcs.borrow().contains_key(n)
132                || e.diags.borrow().contains_key(n)
133                || e.inputs.borrow().contains_key(n)
134                || e.outputs.borrow().iter().any(|(o, _, _)| o == n)
135                || e.imports.borrow().contains_key(n)
136                || e.namespaces.borrow().contains_key(n)
137        };
138        for d in &m.decls {
139            match &d.body {
140                DeclBody::Import { from, names, ns } => {
141                    let Some(tm) = targets.get(from) else {
142                        continue;
143                    };
144                    if let Some(ns) = ns {
145                        if taken(ns) {
146                            self.report(
147                                "E3006",
148                                format!(
149                                    "import {ns} collides with an existing binding in {}",
150                                    abs.display()
151                                ),
152                            );
153                            continue;
154                        }
155                        m.env
156                            .namespaces
157                            .borrow_mut()
158                            .insert(ns.clone(), (tm.env.clone(), tm.exports.clone()));
159                        continue;
160                    }
161                    for it in names.iter().flatten() {
162                        let local = it.alias.clone().unwrap_or_else(|| it.name.clone());
163                        let ex = tm.exports.borrow().get(&it.name).cloned();
164                        let Some(ex) = ex else {
165                            self.report(
166                                "E3005",
167                                format!("{} does not export {}", tm.path.display(), it.name),
168                            );
169                            continue;
170                        };
171                        if taken(&local) {
172                            self.report(
173                                "E3006",
174                                format!(
175                                    "import {local} collides with an existing binding in {}",
176                                    abs.display()
177                                ),
178                            );
179                            continue;
180                        }
181                        m.env.imports.borrow_mut().insert(local, ex);
182                    }
183                }
184                DeclBody::ReExport { from, names } => {
185                    let Some(tm) = targets.get(from) else {
186                        continue;
187                    };
188                    for it in names {
189                        let ex = tm.exports.borrow().get(&it.name).cloned();
190                        match ex {
191                            Some(ex) => {
192                                m.exports.borrow_mut().insert(
193                                    it.alias.clone().unwrap_or_else(|| it.name.clone()),
194                                    ex,
195                                );
196                            }
197                            None => self.report(
198                                "E3005",
199                                format!("{} does not export {}", tm.path.display(), it.name),
200                            ),
201                        }
202                    }
203                }
204                _ => {}
205            }
206        }
207        for d in &m.decls {
208            if !d.exported {
209                continue;
210            }
211            if matches!(
212                d.body,
213                DeclBody::Unit { .. }
214                    | DeclBody::Dimension { .. }
215                    | DeclBody::Import { .. }
216                    | DeclBody::ReExport { .. }
217            ) {
218                continue;
219            }
220            if let Some(n) = d.name() {
221                m.exports.borrow_mut().insert(
222                    n.to_string(),
223                    Export {
224                        env: m.env.clone(),
225                        name: n.to_string(),
226                    },
227                );
228            }
229        }
230        self.order.push(m.clone());
231        Some(m)
232    }
233}
234
235fn normalize(p: &Path) -> PathBuf {
236    let mut out = PathBuf::new();
237    for c in p.components() {
238        match c {
239            std::path::Component::ParentDir => {
240                out.pop();
241            }
242            std::path::Component::CurDir => {}
243            other => out.push(other),
244        }
245    }
246    out
247}
248
249/// `resolver` maps package specifiers to paths (packages, §8.6);
250/// `overrides` maps absolute paths to buffer contents (editors)
251pub fn load_modules(
252    entry: &Path,
253    resolver: Option<&Resolver>,
254    overrides: Option<&HashMap<PathBuf, String>>,
255) -> LoadResult {
256    let mut ld = Loader {
257        modules: HashMap::new(),
258        order: vec![],
259        visiting: vec![],
260        diags: vec![],
261        resolver: resolver.cloned(),
262        overrides: overrides.cloned().unwrap_or_default(),
263    };
264    let entry_m = ld.load(entry);
265    if let Some(e) = &entry_m {
266        link_universe(&ld.order, e, &mut ld.diags);
267    }
268    LoadResult {
269        modules: ld.order,
270        entry: entry_m,
271        diags: ld.diags,
272    }
273}
274
275fn link_universe(mods: &[Rc<Module>], entry: &Rc<Module>, diags: &mut Vec<Diag>) {
276    let mut owners: HashMap<String, PathBuf> = HashMap::new();
277    for m in mods {
278        for d in &m.decls {
279            if let DeclBody::Output { name, .. } | DeclBody::Input { name, .. } = &d.body {
280                if let Some(prev) = owners.get(name) {
281                    if *prev != m.path {
282                        diags.push(Diag::error(
283                            format!(
284                                "root {name} declared in both {} and {}",
285                                prev.display(),
286                                m.path.display()
287                            ),
288                            String::new(),
289                            Some("E3018"),
290                        ));
291                    }
292                }
293                owners.insert(name.clone(), m.path.clone());
294            }
295        }
296    }
297    for m in mods {
298        for d in &m.decls {
299            if !d.exported {
300                continue;
301            }
302            match &d.body {
303                DeclBody::Dimension { name, terms } => {
304                    for m2 in mods {
305                        if Rc::ptr_eq(m2, m) {
306                            continue;
307                        }
308                        let own = m2.decls.iter().any(
309                            |x| matches!(&x.body, DeclBody::Dimension { name: n, .. } if n == name),
310                        );
311                        let has = m2.env.dim_decls.borrow().contains_key(name);
312                        if has && !own {
313                            continue;
314                        }
315                        if has {
316                            diags.push(Diag::error(
317                                format!("dimension {name} redeclared across modules"),
318                                String::new(),
319                                Some("E3001"),
320                            ));
321                        } else {
322                            m2.env
323                                .dim_decls
324                                .borrow_mut()
325                                .insert(name.clone(), terms.clone());
326                        }
327                    }
328                }
329                DeclBody::Unit {
330                    name,
331                    dim,
332                    factor,
333                    base,
334                } => {
335                    for m2 in mods {
336                        if Rc::ptr_eq(m2, m) {
337                            continue;
338                        }
339                        let own = m2.decls.iter().any(
340                            |x| matches!(&x.body, DeclBody::Unit { name: n, .. } if n == name),
341                        );
342                        let has = m2.env.unit_decls.borrow().contains_key(name);
343                        if has && !own {
344                            continue;
345                        }
346                        if has {
347                            diags.push(Diag::error(
348                                format!("unit {name} redeclared across modules"),
349                                String::new(),
350                                Some("E4073"),
351                            ));
352                        } else {
353                            m2.env.unit_decls.borrow_mut().insert(
354                                name.clone(),
355                                UnitDecl {
356                                    dim: dim.clone(),
357                                    factor: factor.clone(),
358                                    base: base.clone(),
359                                },
360                            );
361                        }
362                    }
363                }
364                _ => {}
365            }
366        }
367    }
368    for m in mods {
369        if Rc::ptr_eq(m, entry) {
370            continue;
371        }
372        *m.env.registry.borrow_mut() = entry.env.registry.borrow().clone();
373        *m.env.roots.borrow_mut() = entry.env.roots.borrow().clone();
374        *m.env.diagnostics.borrow_mut() = entry.env.diagnostics.borrow().clone();
375    }
376}
377
378/// a document bound to an input (§10): `module` is the one declaring the
379/// input (the entry when unset)
380pub struct Bind {
381    pub module: Option<Rc<Module>>,
382    pub input: String,
383    pub raw: Value,
384}
385
386pub fn run_universe(
387    mods: &[Rc<Module>],
388    entry: &Rc<Module>,
389    binds: Vec<Bind>,
390) -> (Rc<Engine>, Vec<Diag>) {
391    let eng = Engine::new(entry.env.clone());
392    for m in mods {
393        eng.install_hooks(&m.env, true);
394    }
395    // bound documents first: an output may read an input (§5.5), and a
396    // bound input is a root of the universe (§9.2); unbound inputs with a
397    // fallback bind on first demand (§9.4)
398    for b in binds {
399        let m = b.module.clone().unwrap_or_else(|| entry.clone());
400        let decl = m.env.inputs.borrow().get(&b.input).cloned();
401        let Some((ty_ast, _)) = decl else { continue };
402        let sc = Scope::new(&b.input, Some(m.env.clone()));
403        match m.env.resolve(&ty_ast, None) {
404            Ok(rt) => eng.bind_root(&b.input, RootSrc::Doc(b.raw), &rt, &sc),
405            Err(e) => entry.env.report(Diag::error(e, b.input.clone(), None)),
406        }
407    }
408    for m in mods {
409        let outs = m.env.outputs.borrow().clone();
410        for (name, ty_ast, expr) in outs {
411            let sc = Scope::new(&name, Some(m.env.clone()));
412            match m.env.resolve(&ty_ast, None) {
413                Ok(rt) => eng.bind_root(&name, RootSrc::Expr(&expr), &rt, &sc),
414                Err(e) => entry.env.report(Diag::error(e, name.clone(), None)),
415            }
416        }
417    }
418    eng.drive(&entry.env);
419    // §6.7: evaluation- and validation-time diagnostics in (path, id) order
420    let diags = sort_diags(entry.env.diagnostics_vec());
421    entry.env.diag_set(diags.clone());
422    (eng, diags)
423}