Skip to main content

aura_lang/vfs/
loader.rs

1//! Module tree loader (SPEC §5.3): DFS with coloring (Loading/Loaded),
2//! a cache of evaluated modules (each module is lexed/parsed/evaluated exactly
3//! once), aura.lock integration, and capability isolation for imports (D1).
4
5use std::collections::HashMap;
6
7use super::lockfile::{integrity_of, LockEntry, Lockfile};
8use super::{parse_version, version_satisfies, FileResolver, ImportSpec, ModuleId};
9use crate::error::Diagnostic;
10use crate::eval::value::Value;
11use crate::eval::Interpreter;
12use crate::lexer::Lexer;
13use crate::parser::ast::ImportSource;
14use crate::parser::Parser;
15use crate::source::SourceCache;
16use crate::span::Span;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19enum LoadState {
20    Loading,
21    Loaded,
22}
23
24pub struct Loader<'a, 'r> {
25    pub cache: &'a SourceCache,
26    pub resolver: &'r dyn FileResolver,
27    pub lock: Lockfile,
28    /// --frozen (CI): no matching lock entry is an E0403 error.
29    pub frozen: bool,
30    /// --hermetic: every effectful call is an analysis error (E0505), in every
31    /// module including the root.
32    pub hermetic: bool,
33    /// Static analysis diagnostics for ALL loaded modules (SPEC §6.1):
34    /// warnings accumulate here; analysis errors abort loading the module.
35    pub diags: Vec<Diagnostic>,
36    state: HashMap<ModuleId, LoadState>,
37    values: HashMap<ModuleId, Value<'a>>,
38    chain: Vec<ModuleId>,
39}
40
41fn rt(code: &'static str, msg: impl Into<String>, span: Span) -> Diagnostic {
42    Diagnostic::error(code, msg, span, "while loading modules")
43}
44
45impl<'a, 'r> Loader<'a, 'r> {
46    pub fn new(cache: &'a SourceCache, resolver: &'r dyn FileResolver) -> Self {
47        Loader {
48            cache,
49            resolver,
50            lock: Lockfile::default(),
51            frozen: false,
52            hermetic: false,
53            diags: Vec::new(),
54            state: HashMap::new(),
55            values: HashMap::new(),
56            chain: Vec::new(),
57        }
58    }
59
60    /// Entry point: the root module receives the interpreter's I/O capabilities.
61    pub fn eval_entry(
62        &mut self,
63        interp: &mut Interpreter<'a>,
64        spec: &ImportSpec<'_>,
65    ) -> Result<Value<'a>, Diagnostic> {
66        let dummy = Span::new(0, 0, 0);
67        let id = self
68            .resolver
69            .resolve(spec, None)
70            .map_err(|e| rt("E0404", e, dummy))?;
71        self.eval_module_rec(interp, id, dummy, true)
72    }
73
74    fn eval_module_rec(
75        &mut self,
76        interp: &mut Interpreter<'a>,
77        id: ModuleId,
78        err_span: Span,
79        is_root: bool,
80    ) -> Result<Value<'a>, Diagnostic> {
81        match self.state.get(&id) {
82            Some(LoadState::Loaded) => return Ok(self.values[&id].clone()),
83            Some(LoadState::Loading) => {
84                let mut chain: Vec<String> = self.chain.iter().map(ModuleId::to_string).collect();
85                chain.push(id.to_string());
86                return Err(rt(
87                    "E0401",
88                    format!("cyclic import: {}", chain.join(" -> ")),
89                    err_span,
90                ));
91            }
92            None => {}
93        }
94        self.state.insert(id.clone(), LoadState::Loading);
95        self.chain.push(id.clone());
96
97        let result = self.eval_module_inner(interp, &id, err_span, is_root);
98
99        self.chain.pop();
100        match &result {
101            Ok(v) => {
102                self.state.insert(id.clone(), LoadState::Loaded);
103                self.values.insert(id, v.clone());
104            }
105            Err(_) => {
106                self.state.remove(&id);
107            }
108        }
109        result
110    }
111
112    fn eval_module_inner(
113        &mut self,
114        interp: &mut Interpreter<'a>,
115        id: &ModuleId,
116        err_span: Span,
117        is_root: bool,
118    ) -> Result<Value<'a>, Diagnostic> {
119        let text = self
120            .resolver
121            .load(id)
122            .map_err(|e| rt("E0404", format!("cannot load module '{id}': {e}"), err_span))?;
123        self.verify_lock(id, &text, err_span)?;
124
125        let (source_id, src) = self.cache.add(id.to_string(), text);
126        let toks = Lexer::new(src, source_id).tokenize()?;
127        let module = match Parser::new(toks).parse_module() {
128            Ok(m) => m,
129            Err(mut ds) => {
130                let first = ds.remove(0);
131                self.diags.extend(ds);
132                return Err(first);
133            }
134        };
135
136        // Static analysis of every loaded module (SPEC §6.1): errors
137        // (E0504 etc.) block the module before evaluation; warnings (W0501,
138        // W0512 in imports) accumulate in self.diags — the host decides the strict policy.
139        let mut analysis = crate::analysis::analyze_with(&module, is_root, self.hermetic);
140        if let Some(pos) = analysis
141            .iter()
142            .position(|d| d.severity == crate::error::Severity::Error)
143        {
144            let first = analysis.remove(pos);
145            self.diags.extend(analysis);
146            return Err(first);
147        }
148        self.diags.extend(analysis);
149
150        // First recursively evaluate all imports, then publish the aliases:
151        // child modules overwrite the global alias map during their own eval.
152        let mut resolved: Vec<(&str, Value<'a>)> = Vec::with_capacity(module.imports.len());
153        for imp in &module.imports {
154            let spec = match &imp.source {
155                ImportSource::File(p) => ImportSpec::File(p),
156                ImportSource::Registry { path, version } => ImportSpec::Registry { path, version },
157            };
158            let child_id = self.resolve_with_lock(&spec, Some(id), imp.span)?;
159            let value = self.eval_module_rec(interp, child_id, imp.span, false)?;
160            resolved.push((imp.alias, value));
161        }
162        for (alias, value) in resolved {
163            interp.provide_module(alias, value);
164        }
165
166        // D1: imported modules have no I/O capabilities (SPEC §4.3).
167        let saved_root = interp.current_root;
168        interp.current_root = is_root;
169        let result = interp.eval_module(&module);
170        interp.current_root = saved_root;
171        result
172    }
173
174    /// Resolution prioritizes aura.lock (SPEC §5.2): a matching lock entry wins;
175    /// --frozen forbids resolving around the lock (E0403).
176    fn resolve_with_lock(
177        &mut self,
178        spec: &ImportSpec<'_>,
179        importer: Option<&ModuleId>,
180        span: Span,
181    ) -> Result<ModuleId, Diagnostic> {
182        if let ImportSpec::Registry { path, version } = spec {
183            let request = parse_version(version)
184                .ok_or_else(|| rt("E0404", format!("malformed version '{version}'"), span))?;
185            if let Some(entry) = self.lock.entries.get(*path) {
186                if parse_version(&entry.version)
187                    .is_some_and(|locked| version_satisfies(&request, &locked))
188                {
189                    return Ok(ModuleId::Registry {
190                        path: path.to_string(),
191                        version: entry.version.clone(),
192                    });
193                }
194            }
195            if self.frozen {
196                return Err(rt(
197                    "E0403",
198                    format!("--frozen: aura.lock has no entry satisfying '{path}@{version}'"),
199                    span,
200                ));
201            }
202        }
203        self.resolver
204            .resolve(spec, importer)
205            .map_err(|e| rt("E0404", e, span))
206    }
207
208    /// Integrity check for registry modules (E0402) and appending to the lock.
209    fn verify_lock(&mut self, id: &ModuleId, text: &str, span: Span) -> Result<(), Diagnostic> {
210        let ModuleId::Registry { path, version } = id else {
211            return Ok(());
212        };
213        let hash = integrity_of(text);
214        match self.lock.entries.get(path) {
215            Some(entry) if entry.version == *version => {
216                // A lock written before token-stream hashing holds a `sha256-`
217                // entry. Verify it the way it was written — failing an old lock
218                // over an algorithm change would be indistinguishable from a real
219                // tampering report, which is the one thing this must never be.
220                let expected = crate::vfs::lockfile::recompute_like(&entry.integrity, text);
221                if entry.integrity != expected {
222                    return Err(rt(
223                        "E0402",
224                        format!(
225                            "integrity mismatch for '{path}@v{version}': lock has {}, got {expected}",
226                            entry.integrity
227                        ),
228                        span,
229                    ));
230                }
231                // Verified under the old algorithm: upgrade the entry so the next
232                // reformat of the package does not look like tampering. Under
233                // --frozen the lock is read-only, so it stays as it is.
234                if expected != hash && !self.frozen {
235                    self.lock.entries.insert(
236                        path.clone(),
237                        LockEntry {
238                            version: version.clone(),
239                            integrity: hash,
240                        },
241                    );
242                    self.lock.dirty = true;
243                }
244            }
245            _ => {
246                if self.frozen {
247                    return Err(rt(
248                        "E0403",
249                        format!("--frozen: '{path}@v{version}' is not in aura.lock"),
250                        span,
251                    ));
252                }
253                self.lock.entries.insert(
254                    path.clone(),
255                    LockEntry {
256                        version: version.clone(),
257                        integrity: hash,
258                    },
259                );
260                self.lock.dirty = true;
261            }
262        }
263        Ok(())
264    }
265}