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
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
84#[derive(Clone, Debug)]
90pub struct LoadedModule {
91 pub dep_name: String,
92 pub items: Vec<TopLevel>,
93 pub path: PathBuf,
94}
95
96pub 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 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
207pub 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#[cfg(test)]
293mod tests {
294 use super::{parse_source, require_module_declaration};
295
296 #[test]
297 fn require_module_accepts_single_first_module() {
298 let src = "module Demo\n intent = \"ok\"\nfn x() -> Int\n 1\n";
299 let items = parse_source(src).expect("parse");
300 require_module_declaration(&items, "demo.av").expect("module declaration should pass");
301 }
302
303 #[test]
304 fn require_module_rejects_missing_module() {
305 let src = "fn x() -> Int\n 1\n";
306 let items = parse_source(src).expect("parse");
307 let err = require_module_declaration(&items, "demo.av").expect_err("expected error");
308 assert!(err.contains("must declare `module <Name>`"));
309 }
310
311 #[test]
312 fn require_module_rejects_module_not_first() {
313 let src = "fn x() -> Int\n 1\nmodule Demo\n";
314 let items = parse_source(src).expect("parse");
315 let err = require_module_declaration(&items, "demo.av").expect_err("expected error");
316 assert!(err.contains("must place `module <Name>` as the first"));
317 }
318
319 #[test]
320 fn require_module_rejects_multiple_modules() {
321 let src = "module A\nmodule B\n";
322 let items = parse_source(src).expect("parse");
323 let err = require_module_declaration(&items, "demo.av").expect_err("expected error");
324 assert!(err.contains("exactly one module declaration"));
325 }
326
327 #[test]
328 fn parse_rejects_record_positional_pattern() {
329 let src = "module Demo\nrecord User\n name: String\nfn f(u: User) -> String\n match u\n User(name) -> name\n";
330 let err = parse_source(src).expect_err("record positional patterns should be rejected");
331 assert!(err.contains("bind the whole value with a lower-case name"));
332 }
333
334 #[test]
335 fn parse_rejects_unqualified_constructor_pattern() {
336 let src = "module Demo\ntype Shape\n Circle(Int)\nfn f(s: Shape) -> Int\n match s\n Circle(r) -> r\n";
337 let err =
338 parse_source(src).expect_err("unqualified constructor patterns should be rejected");
339 assert!(err.contains("Constructor patterns must be qualified"));
340 }
341}