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
16pub 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
84pub struct LoadedModule {
90 pub dep_name: String,
91 pub items: Vec<TopLevel>,
92 pub path: PathBuf,
93}
94
95pub 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}