1use std::collections::{HashMap, HashSet};
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use husk_ast::{ExternItemKind, File, Ident, Item, ItemKind, TypeExpr, TypeExprKind, Visibility};
6use husk_parser::parse_str;
7
8#[derive(Debug, Clone)]
9pub struct Module {
10 pub file: File,
11}
12
13#[derive(Debug)]
14pub struct ModuleGraph {
15 pub modules: HashMap<Vec<String>, Module>,
16}
17
18#[derive(Debug, thiserror::Error)]
19pub enum LoadError {
20 #[error("failed to resolve {0}: {1}")]
21 Io(String, String),
22 #[error("parse errors in {path}")]
23 Parse {
24 path: String,
25 source_code: String,
26 errors: Vec<husk_parser::ParseError>,
27 },
28 #[error("cycle detected involving {0}")]
29 Cycle(String),
30 #[error("missing module {0}")]
31 Missing(String),
32}
33
34pub fn load_graph(entry: &Path) -> Result<ModuleGraph, LoadError> {
37 let entry_abs = canonical(entry)?;
38 let root = entry_abs
39 .parent()
40 .ok_or_else(|| LoadError::Io(entry.display().to_string(), "no parent".into()))?
41 .to_path_buf();
42
43 let mut modules = HashMap::new();
44 let mut visiting = HashSet::new();
45 let mut order = Vec::new();
46
47 dfs_load(
48 &entry_abs,
49 &root,
50 vec!["crate".into()],
51 &mut modules,
52 &mut visiting,
53 &mut order,
54 )?;
55
56 Ok(ModuleGraph { modules })
57}
58
59fn dfs_load(
60 path: &Path,
61 root: &Path,
62 module_path: Vec<String>,
63 modules: &mut HashMap<Vec<String>, Module>,
64 visiting: &mut HashSet<Vec<String>>,
65 order: &mut Vec<Vec<String>>,
66) -> Result<(), LoadError> {
67 if modules.contains_key(&module_path) {
68 return Ok(());
69 }
70 if !visiting.insert(module_path.clone()) {
71 return Err(LoadError::Cycle(module_path.join("::")));
72 }
73
74 let src = fs::read_to_string(path)
75 .map_err(|e| LoadError::Io(path.display().to_string(), e.to_string()))?;
76 let parsed = parse_str(&src);
77 if !parsed.errors.is_empty() {
78 return Err(LoadError::Parse {
79 path: path.display().to_string(),
80 source_code: src.clone(),
81 errors: parsed.errors,
82 });
83 }
84 let file = parsed.file.ok_or_else(|| LoadError::Parse {
85 path: path.display().to_string(),
86 source_code: src.clone(),
87 errors: Vec::new(),
88 })?;
89
90 for dep_mod_path in discover_module_paths(&file) {
92 let dep_fs_path = module_path_to_file(root, &dep_mod_path)
93 .ok_or_else(|| LoadError::Missing(dep_mod_path.join("::")))?;
94 if !dep_fs_path.exists() {
95 return Err(LoadError::Missing(dep_mod_path.join("::")));
96 }
97 dfs_load(&dep_fs_path, root, dep_mod_path, modules, visiting, order)?;
98 }
99
100 modules.insert(
101 module_path.clone(),
102 Module {
103 file: file.clone(),
104 },
105 );
106 order.push(module_path.clone());
107 visiting.remove(&module_path);
108 Ok(())
109}
110
111fn canonical(path: &Path) -> Result<PathBuf, LoadError> {
112 path.canonicalize()
113 .map_err(|e| LoadError::Io(path.display().to_string(), e.to_string()))
114}
115
116fn module_path_to_file(root: &Path, mod_path: &[String]) -> Option<PathBuf> {
117 if mod_path.is_empty() {
118 return None;
119 }
120 if mod_path[0] != "crate" {
121 return None;
122 }
123 if mod_path.len() == 1 {
124 return None;
125 }
126 let rel_parts = &mod_path[1..];
127 let mut p = root.to_path_buf();
128 for (i, part) in rel_parts.iter().enumerate() {
129 if i + 1 == rel_parts.len() {
130 p.push(format!("{part}.hk"));
131 } else {
132 p.push(part);
133 }
134 }
135 Some(p)
136}
137
138fn discover_module_paths(file: &File) -> Vec<Vec<String>> {
139 let mut mods = Vec::new();
140 for item in &file.items {
141 if let ItemKind::Use { path, .. } = &item.kind {
142 if path.first().map(|i| i.name.as_str()) == Some("crate") && path.len() >= 2 {
143 let module_seg = path[1].name.clone();
144 let mut mod_path = vec!["crate".to_string(), module_seg];
145 let path_end = path.len() - 1;
149 if path.len() > 2 {
150 mod_path.extend(path[2..path_end].iter().map(|i| i.name.clone()));
151 }
152 mods.push(mod_path);
153 }
154 }
155 }
156 mods
157}
158
159pub fn assemble_root(graph: &ModuleGraph) -> Result<File, LoadError> {
166 let root_mod = graph
167 .modules
168 .get(&vec!["crate".into()])
169 .ok_or_else(|| LoadError::Missing("crate".into()))?;
170 let mut items = Vec::new();
171 let mut seen_names = HashSet::new();
172 let mut imported_types = HashSet::new(); let mut included_extern_blocks = HashSet::new(); for item in root_mod.file.items.iter() {
177 match &item.kind {
178 ItemKind::Use { path, kind } => {
179 match kind {
180 husk_ast::UseKind::Item => {
181 if path.len() < 3 {
183 continue;
184 }
185 let module_path = module_path_from_use(path);
186 let module = graph
187 .modules
188 .get(&module_path)
189 .ok_or_else(|| LoadError::Missing(module_path.join("::")))?;
190 let item_name = path.last().unwrap().name.clone();
191 let export = find_pub_item(&module.file, &item_name).ok_or_else(|| {
192 LoadError::Missing(format!(
193 "`{}` not found or not public in `{}`",
194 item_name,
195 module_path.join("::")
196 ))
197 })?;
198
199 match &export.kind {
201 ItemKind::Struct { name, .. } | ItemKind::Enum { name, .. } => {
202 imported_types.insert(name.name.clone());
203 }
204 ItemKind::ExternBlock { items: ext_items, .. } => {
205 for ext in ext_items {
207 if let ExternItemKind::Struct { name, .. } = &ext.kind {
208 if name.name == item_name {
209 imported_types.insert(name.name.clone());
210 }
211 }
212 }
213 }
214 _ => {}
215 }
216
217 if !seen_names.insert(item_name.clone()) {
218 return Err(LoadError::Missing(format!(
219 "conflicting import `{}` in root module",
220 item_name
221 )));
222 }
223
224 if let ItemKind::ExternBlock { items: ext_items, .. } = &export.kind {
227 let all_structs_included = ext_items.iter().all(|ext| {
229 if let ExternItemKind::Struct { name, .. } = &ext.kind {
230 included_extern_blocks.contains(&name.name)
231 } else {
232 true
233 }
234 });
235
236 if !all_structs_included {
237 for ext in ext_items {
239 if let ExternItemKind::Struct { name, .. } = &ext.kind {
240 included_extern_blocks.insert(name.name.clone());
241 }
242 }
243 items.push(export.clone());
244 }
245 } else {
246 items.push(export.clone());
247 }
248 }
249 husk_ast::UseKind::Glob | husk_ast::UseKind::Variants(_) => {
250 items.push(item.clone());
254 }
255 }
256 }
257 _ => {
258 if let Some(name) = top_level_name(item) {
259 if seen_names.insert(name.clone()) {
260 items.push(item.clone());
261 } else {
262 return Err(LoadError::Missing(format!(
263 "duplicate top-level definition `{}`",
264 name
265 )));
266 }
267 } else {
268 items.push(item.clone());
269 }
270 }
271 }
272 }
273
274 for (module_path, module) in &graph.modules {
276 if module_path == &vec!["crate".to_string()] {
278 continue;
279 }
280
281 for item in &module.file.items {
282 match &item.kind {
283 ItemKind::ExternBlock { items: ext_items, .. } => {
285 let has_imported_type = ext_items.iter().any(|ext| {
286 if let ExternItemKind::Struct { name, .. } = &ext.kind {
287 imported_types.contains(&name.name)
288 } else {
289 false
290 }
291 });
292
293 if has_imported_type {
294 let mut should_include = false;
296 for ext in ext_items {
297 if let ExternItemKind::Struct { name, .. } = &ext.kind {
298 if !included_extern_blocks.contains(&name.name) {
299 included_extern_blocks.insert(name.name.clone());
300 should_include = true;
301 }
302 }
303 }
304 if should_include {
306 let already_included = items.iter().any(|existing| {
308 if let ItemKind::ExternBlock {
309 items: existing_items,
310 ..
311 } = &existing.kind
312 {
313 existing_items.len() == ext_items.len()
315 && existing_items.iter().zip(ext_items.iter()).all(
316 |(a, b)| match (&a.kind, &b.kind) {
317 (
318 ExternItemKind::Struct { name: n1, .. },
319 ExternItemKind::Struct { name: n2, .. },
320 ) => n1.name == n2.name,
321 _ => false,
322 },
323 )
324 } else {
325 false
326 }
327 });
328
329 if !already_included {
330 items.push(item.clone());
331 }
332 }
333 }
334 }
335 ItemKind::Impl(impl_block) => {
337 let self_ty_name = type_expr_to_name(&impl_block.self_ty);
338 if imported_types.contains(&self_ty_name) {
339 items.push(item.clone());
340 }
341 }
342 _ => {}
343 }
344 }
345 }
346
347 Ok(File { items })
348}
349
350fn module_path_from_use(path: &[Ident]) -> Vec<String> {
351 let mut out = Vec::new();
352 for seg in &path[..path.len() - 1] {
353 out.push(seg.name.clone());
354 }
355 out
356}
357
358fn find_pub_item<'a>(file: &'a File, name: &str) -> Option<&'a Item> {
359 if let Some(item) = file
361 .items
362 .iter()
363 .find(|it| it.visibility == Visibility::Public && matches_name(it, name))
364 {
365 return Some(item);
366 }
367
368 for item in &file.items {
370 if let ItemKind::ExternBlock { items: ext_items, .. } = &item.kind {
371 for ext in ext_items {
372 if matches_extern_item_name(&ext.kind, name) {
373 return Some(item);
375 }
376 }
377 }
378 }
379
380 None
381}
382
383fn matches_name(item: &Item, name: &str) -> bool {
384 match &item.kind {
385 ItemKind::Fn { name: n, .. } => n.name == name,
386 ItemKind::Struct { name: n, .. } => n.name == name,
387 ItemKind::Enum { name: n, .. } => n.name == name,
388 ItemKind::TypeAlias { name: n, .. } => n.name == name,
389 ItemKind::Trait(trait_def) => trait_def.name.name == name,
390 ItemKind::Impl(_) => false, ItemKind::ExternBlock { .. } => false,
392 ItemKind::Use { .. } => false,
393 }
394}
395
396fn top_level_name(item: &Item) -> Option<String> {
397 match &item.kind {
398 ItemKind::Fn { name, .. } => Some(name.name.clone()),
399 ItemKind::Struct { name, .. } => Some(name.name.clone()),
400 ItemKind::Enum { name, .. } => Some(name.name.clone()),
401 ItemKind::TypeAlias { name, .. } => Some(name.name.clone()),
402 _ => None,
403 }
404}
405
406fn type_expr_to_name(ty: &TypeExpr) -> String {
408 match &ty.kind {
409 TypeExprKind::Named(ident) => ident.name.clone(),
410 TypeExprKind::Generic { name, .. } => name.name.clone(),
411 TypeExprKind::Function { .. } => String::new(),
412 TypeExprKind::Array(elem) => format!("[{}]", type_expr_to_name(elem)),
413 TypeExprKind::Tuple(types) => {
414 let type_names: Vec<String> = types.iter().map(type_expr_to_name).collect();
415 format!("({})", type_names.join(", "))
416 }
417 }
418}
419
420fn matches_extern_item_name(kind: &ExternItemKind, name: &str) -> bool {
422 match kind {
423 ExternItemKind::Struct { name: n, .. } => n.name == name,
424 ExternItemKind::Fn { name: n, .. } => n.name == name,
425 ExternItemKind::Mod { binding, .. } => binding.name == name,
426 ExternItemKind::Static { name: n, .. } => n.name == name,
427 }
428}