Skip to main content

aver/
source.rs

1use std::collections::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.
89pub struct LoadedModule {
90    pub dep_name: String,
91    pub items: Vec<TopLevel>,
92    pub path: PathBuf,
93}
94
95/// Load a dependency tree starting from `root_deps`.
96/// Returns modules in dependency order (leaves first).
97/// Validates module declarations and detects circular imports.
98pub fn load_module_tree(
99    root_deps: &[String],
100    module_root: &str,
101) -> Result<Vec<LoadedModule>, String> {
102    let mut result = Vec::new();
103    let mut loaded = HashSet::new();
104    let mut loading = Vec::new();
105    for dep in root_deps {
106        load_recursive(dep, module_root, &mut loaded, &mut loading, &mut result)?;
107    }
108    Ok(result)
109}
110
111fn load_recursive(
112    dep_name: &str,
113    module_root: &str,
114    loaded: &mut HashSet<String>,
115    loading: &mut Vec<String>,
116    result: &mut Vec<LoadedModule>,
117) -> Result<(), String> {
118    let path = find_module_file(dep_name, module_root)
119        .ok_or_else(|| format!("Module '{}' not found in '{}'", dep_name, module_root))?;
120    let canon = canonicalize_path(&path).to_string_lossy().to_string();
121
122    if loaded.contains(&canon) {
123        return Ok(());
124    }
125    if loading.contains(&canon) {
126        let chain: Vec<String> = loading
127            .iter()
128            .map(|k| {
129                Path::new(k)
130                    .file_stem()
131                    .and_then(|s| s.to_str())
132                    .unwrap_or(k)
133                    .to_string()
134            })
135            .chain(std::iter::once(
136                Path::new(&canon)
137                    .file_stem()
138                    .and_then(|s| s.to_str())
139                    .unwrap_or(&canon)
140                    .to_string(),
141            ))
142            .collect();
143        return Err(format!("Circular import: {}", chain.join(" -> ")));
144    }
145    loading.push(canon.clone());
146
147    let source = std::fs::read_to_string(&path)
148        .map_err(|e| format!("Cannot read '{}': {}", path.display(), e))?;
149    let items =
150        parse_source(&source).map_err(|e| format!("Parse error in '{}': {}", dep_name, e))?;
151
152    require_module_declaration(&items, &path.to_string_lossy())?;
153
154    if let Some(module) = visibility::module_decl(&items) {
155        let expected = dep_name.rsplit('.').next().unwrap_or(dep_name);
156        if module.name != expected {
157            return Err(format!(
158                "Module name mismatch: expected '{}' (from '{}'), found '{}' in '{}'",
159                expected,
160                dep_name,
161                module.name,
162                path.display()
163            ));
164        }
165        for sub_dep in &module.depends {
166            load_recursive(sub_dep, module_root, loaded, loading, result)?;
167        }
168    }
169
170    loading.pop();
171    loaded.insert(canon);
172    result.push(LoadedModule {
173        dep_name: dep_name.to_string(),
174        items,
175        path,
176    });
177    Ok(())
178}
179
180#[cfg(test)]
181mod tests {
182    use super::{parse_source, require_module_declaration};
183
184    #[test]
185    fn require_module_accepts_single_first_module() {
186        let src = "module Demo\n    intent = \"ok\"\nfn x() -> Int\n    1\n";
187        let items = parse_source(src).expect("parse");
188        require_module_declaration(&items, "demo.av").expect("module declaration should pass");
189    }
190
191    #[test]
192    fn require_module_rejects_missing_module() {
193        let src = "fn x() -> Int\n    1\n";
194        let items = parse_source(src).expect("parse");
195        let err = require_module_declaration(&items, "demo.av").expect_err("expected error");
196        assert!(err.contains("must declare `module <Name>`"));
197    }
198
199    #[test]
200    fn require_module_rejects_module_not_first() {
201        let src = "fn x() -> Int\n    1\nmodule Demo\n";
202        let items = parse_source(src).expect("parse");
203        let err = require_module_declaration(&items, "demo.av").expect_err("expected error");
204        assert!(err.contains("must place `module <Name>` as the first"));
205    }
206
207    #[test]
208    fn require_module_rejects_multiple_modules() {
209        let src = "module A\nmodule B\n";
210        let items = parse_source(src).expect("parse");
211        let err = require_module_declaration(&items, "demo.av").expect_err("expected error");
212        assert!(err.contains("exactly one module declaration"));
213    }
214
215    #[test]
216    fn parse_rejects_record_positional_pattern() {
217        let src = "module Demo\nrecord User\n    name: String\nfn f(u: User) -> String\n    match u\n        User(name) -> name\n";
218        let err = parse_source(src).expect_err("record positional patterns should be rejected");
219        assert!(err.contains("bind the whole value with a lower-case name"));
220    }
221
222    #[test]
223    fn parse_rejects_unqualified_constructor_pattern() {
224        let src = "module Demo\ntype Shape\n    Circle(Int)\nfn f(s: Shape) -> Int\n    match s\n        Circle(r) -> r\n";
225        let err =
226            parse_source(src).expect_err("unqualified constructor patterns should be rejected");
227        assert!(err.contains("Constructor patterns must be qualified"));
228    }
229}