Skip to main content

aver/
source.rs

1use std::collections::{HashMap, HashSet};
2use std::path::{Path, PathBuf};
3
4use crate::ast::TopLevel;
5use crate::lexer::Lexer;
6use crate::parser::Parser;
7use crate::visibility;
8
9pub fn parse_source(source: &str) -> Result<Vec<TopLevel>, String> {
10    let mut lexer = Lexer::new(source);
11    let tokens = lexer.tokenize().map_err(|e| e.to_string())?;
12    let mut parser = Parser::new(tokens);
13    parser.parse().map_err(|e| e.to_string())
14}
15
16/// Enforce module contract for file-based programs:
17/// exactly one `module` declaration and it must be the first top-level item.
18pub fn require_module_declaration(items: &[TopLevel], file: &str) -> Result<(), String> {
19    let module_positions: Vec<usize> = items
20        .iter()
21        .enumerate()
22        .filter_map(|(idx, item)| matches!(item, TopLevel::Module(_)).then_some(idx))
23        .collect();
24
25    if module_positions.is_empty() {
26        return Err(format!(
27            "File '{}' must declare `module <Name>` as the first top-level item",
28            file
29        ));
30    }
31
32    if module_positions[0] != 0 {
33        return Err(format!(
34            "File '{}' must place `module <Name>` as the first top-level item",
35            file
36        ));
37    }
38
39    if module_positions.len() > 1 {
40        return Err(format!(
41            "File '{}' must contain exactly one module declaration (found {})",
42            file,
43            module_positions.len()
44        ));
45    }
46
47    Ok(())
48}
49
50pub fn find_module_file(name: &str, module_root: &str) -> Option<PathBuf> {
51    let root = Path::new(module_root);
52    let parts: Vec<&str> = name.split('.').filter(|s| !s.is_empty()).collect();
53    if parts.is_empty() {
54        return None;
55    }
56
57    let lower_rel = format!(
58        "{}.av",
59        parts
60            .iter()
61            .map(|p| p.to_lowercase())
62            .collect::<Vec<_>>()
63            .join("/")
64    );
65    let exact_rel = format!("{}.av", parts.join("/"));
66
67    let lower = root.join(&lower_rel);
68    if lower.exists() {
69        return Some(lower);
70    }
71
72    let exact = root.join(&exact_rel);
73    if exact.exists() {
74        return Some(exact);
75    }
76
77    None
78}
79
80pub fn canonicalize_path(path: &Path) -> PathBuf {
81    std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
82}
83
84// ---------------------------------------------------------------------------
85// Shared module loader — find, read, parse, validate, recurse
86// ---------------------------------------------------------------------------
87
88/// A parsed module ready for backend consumption.
89#[derive(Clone, Debug)]
90pub struct LoadedModule {
91    pub dep_name: String,
92    pub items: Vec<TopLevel>,
93    pub path: PathBuf,
94}
95
96/// Sibling of [`load_module_tree`] that resolves dependency modules
97/// from an in-memory file map instead of the filesystem. Used by the
98/// playground so a browser-side virtual fs can compile a multi-file
99/// project without disk IO.
100///
101/// The map's keys must be file paths matching what
102/// [`find_module_file`] would produce (e.g. `"types.av"`,
103/// `"rogue/combat.av"`). Both lowercase and exact casings are tried
104/// for each requested dep, mirroring the on-disk search order.
105pub fn load_module_tree_from_map(
106    root_deps: &[String],
107    files: &HashMap<String, String>,
108) -> Result<Vec<LoadedModule>, String> {
109    let mut result = Vec::new();
110    let mut loaded: HashSet<String> = HashSet::new();
111    let mut loading: Vec<String> = Vec::new();
112    for dep in root_deps {
113        load_recursive_from_map(dep, files, &mut loaded, &mut loading, &mut result)?;
114    }
115    Ok(result)
116}
117
118fn load_recursive_from_map(
119    dep_name: &str,
120    files: &HashMap<String, String>,
121    loaded: &mut HashSet<String>,
122    loading: &mut Vec<String>,
123    result: &mut Vec<LoadedModule>,
124) -> Result<(), String> {
125    let key = find_file_key_in_map(dep_name, files)
126        .ok_or_else(|| format!("Module '{}' not found in virtual fs", dep_name))?;
127
128    if loaded.contains(&key) {
129        return Ok(());
130    }
131    if loading.contains(&key) {
132        let chain = loading
133            .iter()
134            .cloned()
135            .chain(std::iter::once(key.clone()))
136            .collect::<Vec<_>>()
137            .join(" -> ");
138        return Err(format!("Circular import: {}", chain));
139    }
140    loading.push(key.clone());
141
142    let source = files.get(&key).unwrap();
143    let items =
144        parse_source(source).map_err(|e| format!("Parse error in '{}': {}", dep_name, e))?;
145    require_module_declaration(&items, &key)?;
146
147    if let Some(module) = visibility::module_decl(&items) {
148        let expected = dep_name.rsplit('.').next().unwrap_or(dep_name);
149        if module.name != expected {
150            return Err(format!(
151                "Module name mismatch: expected '{}' (from dep '{}'), found '{}' in '{}'",
152                expected, dep_name, module.name, key
153            ));
154        }
155        for sub_dep in &module.depends {
156            load_recursive_from_map(sub_dep, files, loaded, loading, result)?;
157        }
158    }
159
160    loading.pop();
161    loaded.insert(key.clone());
162    result.push(LoadedModule {
163        dep_name: dep_name.to_string(),
164        items,
165        path: PathBuf::from(&key),
166    });
167    Ok(())
168}
169
170fn find_file_key_in_map(dep_name: &str, files: &HashMap<String, String>) -> Option<String> {
171    let parts: Vec<&str> = dep_name.split('.').filter(|s| !s.is_empty()).collect();
172    if parts.is_empty() {
173        return None;
174    }
175    let lower_rel = format!(
176        "{}.av",
177        parts
178            .iter()
179            .map(|p| p.to_lowercase())
180            .collect::<Vec<_>>()
181            .join("/")
182    );
183    let exact_rel = format!("{}.av", parts.join("/"));
184    let last = parts.last().copied().unwrap_or(dep_name);
185    let last_lower = format!("{}.av", last.to_lowercase());
186    let last_exact = format!("{}.av", last);
187
188    for candidate in [&lower_rel, &exact_rel, &last_lower, &last_exact] {
189        if files.contains_key(candidate) {
190            return Some(candidate.clone());
191        }
192    }
193    // Fallback: case-insensitive scan — browsers let users name files
194    // however they like, and dep names are canonical-cased anyway.
195    let wanted = last.to_lowercase();
196    files
197        .keys()
198        .find(|k| {
199            Path::new(k)
200                .file_stem()
201                .and_then(|s| s.to_str())
202                .is_some_and(|stem| stem.eq_ignore_ascii_case(&wanted))
203        })
204        .cloned()
205}
206
207/// Load a dependency tree starting from `root_deps`.
208/// Returns modules in dependency order (leaves first).
209/// Validates module declarations and detects circular imports.
210pub fn load_module_tree(
211    root_deps: &[String],
212    module_root: &str,
213) -> Result<Vec<LoadedModule>, String> {
214    let mut result = Vec::new();
215    let mut loaded = HashSet::new();
216    let mut loading = Vec::new();
217    for dep in root_deps {
218        load_recursive(dep, module_root, &mut loaded, &mut loading, &mut result)?;
219    }
220    Ok(result)
221}
222
223fn load_recursive(
224    dep_name: &str,
225    module_root: &str,
226    loaded: &mut HashSet<String>,
227    loading: &mut Vec<String>,
228    result: &mut Vec<LoadedModule>,
229) -> Result<(), String> {
230    let path = find_module_file(dep_name, module_root)
231        .ok_or_else(|| format!("Module '{}' not found in '{}'", dep_name, module_root))?;
232    let canon = canonicalize_path(&path).to_string_lossy().to_string();
233
234    if loaded.contains(&canon) {
235        return Ok(());
236    }
237    if loading.contains(&canon) {
238        let chain: Vec<String> = loading
239            .iter()
240            .map(|k| {
241                Path::new(k)
242                    .file_stem()
243                    .and_then(|s| s.to_str())
244                    .unwrap_or(k)
245                    .to_string()
246            })
247            .chain(std::iter::once(
248                Path::new(&canon)
249                    .file_stem()
250                    .and_then(|s| s.to_str())
251                    .unwrap_or(&canon)
252                    .to_string(),
253            ))
254            .collect();
255        return Err(format!("Circular import: {}", chain.join(" -> ")));
256    }
257    loading.push(canon.clone());
258
259    let source = std::fs::read_to_string(&path)
260        .map_err(|e| format!("Cannot read '{}': {}", path.display(), e))?;
261    let items =
262        parse_source(&source).map_err(|e| format!("Parse error in '{}': {}", dep_name, e))?;
263
264    require_module_declaration(&items, &path.to_string_lossy())?;
265
266    if let Some(module) = visibility::module_decl(&items) {
267        let expected = dep_name.rsplit('.').next().unwrap_or(dep_name);
268        if module.name != expected {
269            return Err(format!(
270                "Module name mismatch: expected '{}' (from '{}'), found '{}' in '{}'",
271                expected,
272                dep_name,
273                module.name,
274                path.display()
275            ));
276        }
277        for sub_dep in &module.depends {
278            load_recursive(sub_dep, module_root, loaded, loading, result)?;
279        }
280    }
281
282    loading.pop();
283    loaded.insert(canon);
284    result.push(LoadedModule {
285        dep_name: dep_name.to_string(),
286        items,
287        path,
288    });
289    Ok(())
290}
291
292/// Convert pre-loaded modules (parsed virtual-fs items from the
293/// playground / LSP / audit paths) into `ModuleInfo` records suitable
294/// for `PipelineConfig.dep_modules` and `SymbolTable::build`.
295///
296/// Each dep goes through `pipeline::run` with
297/// `TypecheckMode::WithLoaded(&siblings)` so the resulting
298/// `AnalysisResult` populates the same `no_alloc` / recursion facts
299/// the disk-loader path produces. The entry-level pipeline still
300/// handles cross-module typing separately; per-dep analysis here
301/// just unlocks the VM compiler's `no_alloc` fast paths on dep
302/// functions instead of forcing the conservative "assume allocates"
303/// branch.
304pub fn loaded_to_module_info(loaded: &[LoadedModule]) -> Vec<crate::codegen::ModuleInfo> {
305    let neutral_policy = crate::ir::NeutralAllocPolicy;
306    loaded
307        .iter()
308        .map(|m| {
309            let depends = visibility::module_decl(&m.items)
310                .map(|md| md.depends.clone())
311                .unwrap_or_default();
312            let type_defs: Vec<_> = m
313                .items
314                .iter()
315                .filter_map(|i| match i {
316                    TopLevel::TypeDef(td) => Some(td.clone()),
317                    _ => None,
318                })
319                .collect();
320            let fn_defs: Vec<_> = m
321                .items
322                .iter()
323                .filter_map(|i| match i {
324                    TopLevel::FnDef(fd) if fd.name != "main" => Some(fd.clone()),
325                    _ => None,
326                })
327                .collect();
328            // Run the canonical pipeline on a clone of this dep's
329            // items, type-checking against the other loaded modules
330            // as the source of cross-module references. We feed the
331            // analysis result alone back into ModuleInfo; the
332            // pipeline-mutated items themselves stay local — the
333            // entry's pipeline run sees the original `m.items` shape
334            // via WithLoaded just like the typechecker did pre-fix.
335            let mut dep_items = m.items.clone();
336            let pipeline_result = crate::ir::pipeline::run(
337                &mut dep_items,
338                crate::ir::PipelineConfig {
339                    typecheck: Some(crate::ir::TypecheckMode::WithLoaded(loaded)),
340                    run_interp_lower: false,
341                    run_buffer_build: false,
342                    alloc_policy: Some(&neutral_policy),
343                    ..Default::default()
344                },
345            );
346            crate::codegen::ModuleInfo {
347                prefix: m.dep_name.clone(),
348                depends,
349                type_defs,
350                fn_defs,
351                verify_laws: crate::codegen::collect_verify_laws(&m.items),
352                analysis: pipeline_result.analysis,
353            }
354        })
355        .collect()
356}
357
358/// Load every dep module declared by `items`'s `Module.depends`, plus
359/// every transitive dep, into `codegen::ModuleInfo` records ready to
360/// hand to `PipelineConfig.dep_modules`.
361///
362/// Each dep goes through the same canonical pipeline as the entry —
363/// `pipeline::run` with `TypecheckMode::Full { base_dir: module_root }`,
364/// `run_interp_lower: false`, `run_buffer_build: false`, and the
365/// neutral alloc policy. Type errors in any dep surface as `Err`.
366///
367/// Callers that need the legacy `commands.rs` shape (run_interp_lower /
368/// run_buffer_build / self_host_mode flags) still own their local copy;
369/// every other call site — `vm_verify`, `wasm_gc_verify`, `bench/runner`,
370/// the research test — should go through here so the dep-loading
371/// contract stays in one place.
372pub fn load_compile_deps(
373    items: &[TopLevel],
374    module_root: &str,
375) -> Result<Vec<crate::codegen::ModuleInfo>, String> {
376    let module = items.iter().find_map(|i| match i {
377        TopLevel::Module(m) => Some(m),
378        _ => None,
379    });
380    let Some(module) = module else {
381        return Ok(vec![]);
382    };
383    let mut result = Vec::new();
384    let mut loaded = HashSet::new();
385    for dep_name in &module.depends {
386        load_module_recursive_for_compile(dep_name, module_root, &mut result, &mut loaded)?;
387    }
388    Ok(result)
389}
390
391fn load_module_recursive_for_compile(
392    name: &str,
393    module_root: &str,
394    result: &mut Vec<crate::codegen::ModuleInfo>,
395    loaded: &mut HashSet<String>,
396) -> Result<(), String> {
397    if !loaded.insert(name.to_string()) {
398        return Ok(());
399    }
400    let path = find_module_file(name, module_root).ok_or_else(|| {
401        format!(
402            "Cannot find module '{}' in module root '{}'",
403            name, module_root
404        )
405    })?;
406    let source =
407        std::fs::read_to_string(&path).map_err(|e| format!("Read '{}': {}", path.display(), e))?;
408    let mut items =
409        parse_source(&source).map_err(|e| format!("Parse '{}': {}", path.display(), e))?;
410    require_module_declaration(&items, path.to_str().unwrap_or(name))?;
411
412    let neutral_policy = crate::ir::NeutralAllocPolicy;
413    let pipeline_result = crate::ir::pipeline::run(
414        &mut items,
415        crate::ir::PipelineConfig {
416            typecheck: Some(crate::ir::TypecheckMode::Full {
417                base_dir: Some(module_root),
418            }),
419            run_interp_lower: false,
420            run_buffer_build: false,
421            alloc_policy: Some(&neutral_policy),
422            ..Default::default()
423        },
424    );
425    if let Some(tc) = pipeline_result.typecheck.as_ref()
426        && !tc.errors.is_empty()
427    {
428        return Err(format!(
429            "Type errors in dependency module '{}':\n{}",
430            name,
431            tc.errors
432                .iter()
433                .map(|e| format!("  {}:{}: {}", e.line, e.col, e.message))
434                .collect::<Vec<_>>()
435                .join("\n")
436        ));
437    }
438
439    let transitive: Vec<String> = items
440        .iter()
441        .find_map(|i| match i {
442            TopLevel::Module(m) => Some(m.depends.clone()),
443            _ => None,
444        })
445        .unwrap_or_default();
446    for dep in &transitive {
447        load_module_recursive_for_compile(dep, module_root, result, loaded)?;
448    }
449
450    let depends = transitive;
451    let type_defs: Vec<_> = items
452        .iter()
453        .filter_map(|i| match i {
454            TopLevel::TypeDef(td) => Some(td.clone()),
455            _ => None,
456        })
457        .collect();
458    let fn_defs: Vec<_> = items
459        .iter()
460        .filter_map(|i| match i {
461            TopLevel::FnDef(fd) if fd.name != "main" => Some(fd.clone()),
462            _ => None,
463        })
464        .collect();
465    result.push(crate::codegen::ModuleInfo {
466        prefix: name.to_string(),
467        depends,
468        type_defs,
469        fn_defs,
470        verify_laws: crate::codegen::collect_verify_laws(&items),
471        analysis: pipeline_result.analysis,
472    });
473    Ok(())
474}
475
476#[cfg(test)]
477mod tests {
478    use super::{parse_source, require_module_declaration};
479
480    #[test]
481    fn require_module_accepts_single_first_module() {
482        let src = "module Demo\n    intent = \"ok\"\nfn x() -> Int\n    1\n";
483        let items = parse_source(src).expect("parse");
484        require_module_declaration(&items, "demo.av").expect("module declaration should pass");
485    }
486
487    #[test]
488    fn require_module_rejects_missing_module() {
489        let src = "fn x() -> Int\n    1\n";
490        let items = parse_source(src).expect("parse");
491        let err = require_module_declaration(&items, "demo.av").expect_err("expected error");
492        assert!(err.contains("must declare `module <Name>`"));
493    }
494
495    #[test]
496    fn require_module_rejects_module_not_first() {
497        let src = "fn x() -> Int\n    1\nmodule Demo\n";
498        let items = parse_source(src).expect("parse");
499        let err = require_module_declaration(&items, "demo.av").expect_err("expected error");
500        assert!(err.contains("must place `module <Name>` as the first"));
501    }
502
503    #[test]
504    fn require_module_rejects_multiple_modules() {
505        let src = "module A\nmodule B\n";
506        let items = parse_source(src).expect("parse");
507        let err = require_module_declaration(&items, "demo.av").expect_err("expected error");
508        assert!(err.contains("exactly one module declaration"));
509    }
510
511    #[test]
512    fn parse_rejects_record_positional_pattern() {
513        let src = "module Demo\nrecord User\n    name: String\nfn f(u: User) -> String\n    match u\n        User(name) -> name\n";
514        let err = parse_source(src).expect_err("record positional patterns should be rejected");
515        assert!(err.contains("bind the whole value with a lower-case name"));
516    }
517
518    #[test]
519    fn parse_rejects_unqualified_constructor_pattern() {
520        let src = "module Demo\ntype Shape\n    Circle(Int)\nfn f(s: Shape) -> Int\n    match s\n        Circle(r) -> r\n";
521        let err =
522            parse_source(src).expect_err("unqualified constructor patterns should be rejected");
523        assert!(err.contains("Constructor patterns must be qualified"));
524    }
525}