husk_lang/
load.rs

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
34/// Build a module graph starting from `entry` (a .hk file).
35/// Supports `use crate::a::b::Item;` where modules map to `a.hk`, `a/b.hk`, etc.
36pub 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    // Recurse into module deps discovered via `use crate::foo::...`
91    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                // The last segment is always an item (type, function, or enum for variant imports),
146                // so we exclude it. E.g., `use crate::foo::Result::{Ok, Err}` depends on module
147                // `crate::foo` (foo.hk), not `crate::foo::Result` (foo/Result.hk).
148                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
159/// Assemble a single `File` with resolved `use` items applied (namespace-aware).
160/// - Keeps the root module's items
161/// - For `use crate::foo::Bar;`, injects a synthetic alias Item for Bar into root
162/// - Respects `pub`: only imports public items from other modules
163/// - Automatically includes impl blocks for imported types
164/// - Automatically includes extern blocks that declare imported types
165pub 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(); // Track imported type names
173    let mut included_extern_blocks = HashSet::new(); // Track which extern blocks we've added
174
175    // Phase 1: Process use statements and root items
176    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                        // Regular item import: `use crate::foo::Bar;`
182                        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                        // Track if this is a type (for impl block inclusion)
200                        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                                // If we're importing from an extern block, track the type
206                                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                        // For extern blocks, avoid adding the same block multiple times
225                        // (e.g., when importing multiple items from the same extern block)
226                        if let ItemKind::ExternBlock { items: ext_items, .. } = &export.kind {
227                            // Check if all structs in this block have already been included
228                            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                                // Mark all structs as included
238                                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                        // Variant imports are handled in semantic analysis, not here
251                        // We just need to pass them through to the assembled file
252                        // The use statement itself is preserved for semantic analysis
253                        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    // Phase 2: Include extern blocks and impl blocks for imported types from all modules
275    for (module_path, module) in &graph.modules {
276        // Skip root module (already processed)
277        if module_path == &vec!["crate".to_string()] {
278            continue;
279        }
280
281        for item in &module.file.items {
282            match &item.kind {
283                // Include extern blocks that declare imported types
284                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                        // Track extern structs to avoid duplicates
295                        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                        // Only include if we haven't already included these structs
305                        if should_include {
306                            // Check if this exact block is already in items (from find_pub_item)
307                            let already_included = items.iter().any(|existing| {
308                                if let ItemKind::ExternBlock {
309                                    items: existing_items,
310                                    ..
311                                } = &existing.kind
312                                {
313                                    // Same items means same block
314                                    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                // Include impl blocks for imported types
336                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    // First, check regular public items
360    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    // Then, check extern block items (they don't have explicit visibility on individual items)
369    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 the extern block containing this item
374                    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, // Impl blocks don't have names
391        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
406/// Extract the type name from a TypeExpr (for impl block self_ty).
407fn 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
420/// Check if an extern item matches the given name.
421fn 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}