rukalang/modules/
mod.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::path::{Path, PathBuf};
3
4use crate::syntax::ast::{ImportDecl, Program};
5use crate::syntax::{
6    self,
7    ast::{FileId, SourceSpan},
8};
9
10mod error;
11mod qualify;
12
13pub use error::ModuleError;
14
15const STD_MODULE_NAME: &str = "std";
16const STD_MODULE_SOURCE: &str = include_str!("../../std/std.rk");
17const STD_STRING_MODULE_NAME: &str = "std::string";
18const STD_STRING_MODULE_SOURCE: &str = include_str!("../../std/string.rk");
19
20/// Return bundled standard-library module source when available.
21fn bundled_std_module_source(module_path: &str) -> Option<&'static str> {
22    match module_path {
23        STD_MODULE_NAME => Some(STD_MODULE_SOURCE),
24        STD_STRING_MODULE_NAME => Some(STD_STRING_MODULE_SOURCE),
25        _ => None,
26    }
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30enum ResolveMode {
31    Native,
32    Browser,
33}
34
35#[derive(Debug, Clone)]
36struct LoadedModule {
37    module_path: String,
38    source_name: String,
39    source_text: String,
40    file_id: FileId,
41    is_entry: bool,
42}
43
44/// Resolve imports for native compilation starting from an entry file.
45pub fn resolve_for_native(entry_path: &Path, entry_source: &str) -> Result<Program, ModuleError> {
46    resolve_modules(entry_path, entry_source, ResolveMode::Native)
47}
48
49/// Resolve imports for browser compilation starting from an entry file.
50pub fn resolve_for_browser(entry_path: &Path, entry_source: &str) -> Result<Program, ModuleError> {
51    resolve_modules(entry_path, entry_source, ResolveMode::Browser)
52}
53
54/// Resolve and merge module imports from one entry point.
55fn resolve_modules(
56    entry_path: &Path,
57    entry_source: &str,
58    mode: ResolveMode,
59) -> Result<Program, ModuleError> {
60    let source_name = entry_path.display().to_string();
61    let root_dir = entry_path
62        .parent()
63        .map(Path::to_path_buf)
64        .unwrap_or_else(|| PathBuf::from("."));
65
66    let mut loaded = Vec::<LoadedModule>::new();
67    let mut seen = BTreeSet::<String>::new();
68    let mut stack = Vec::<String>::new();
69
70    let mut next_file_id = 0_u32;
71
72    let entry = LoadedModule {
73        module_path: "playground".to_owned(),
74        source_name,
75        source_text: entry_source.to_owned(),
76        file_id: allocate_file_id(&mut next_file_id),
77        is_entry: true,
78    };
79    load_module_recursive(
80        &entry,
81        mode,
82        &root_dir,
83        &mut seen,
84        &mut stack,
85        &mut loaded,
86        &mut next_file_id,
87    )?;
88
89    merge_loaded_modules(&loaded)
90}
91
92/// Allocate one sequential file identifier for source parsing.
93fn allocate_file_id(next_file_id: &mut u32) -> FileId {
94    let current = *next_file_id;
95    *next_file_id = next_file_id
96        .checked_add(1)
97        .expect("module parser file id overflow");
98    FileId::from_u32(current)
99}
100
101/// Load one module and all its recursive imports.
102fn load_module_recursive(
103    module: &LoadedModule,
104    mode: ResolveMode,
105    root_dir: &Path,
106    seen: &mut BTreeSet<String>,
107    stack: &mut Vec<String>,
108    out: &mut Vec<LoadedModule>,
109    next_file_id: &mut u32,
110) -> Result<(), ModuleError> {
111    if seen.contains(&module.module_path) {
112        return Ok(());
113    }
114
115    stack.push(module.module_path.clone());
116    let parsed = parse_module(module)?;
117    ensure_no_duplicate_imports(module, &parsed.imports)?;
118    out.push(module.clone());
119    let _ = seen.insert(module.module_path.clone());
120
121    for import in &parsed.imports {
122        let imported = load_import_module(import, module, mode, root_dir, next_file_id)?;
123        if stack.contains(&imported.module_path) {
124            let mut cycle = stack.clone();
125            cycle.push(imported.module_path.clone());
126            return Err(ModuleError::ImportCycle {
127                cycle: cycle.join(" -> "),
128                source_name: module.source_name.clone(),
129                span: import.span,
130            });
131        }
132        load_module_recursive(&imported, mode, root_dir, seen, stack, out, next_file_id)?;
133    }
134
135    let _ = stack.pop();
136    Ok(())
137}
138
139/// Parse one loaded module into a frontend program.
140fn parse_module(module: &LoadedModule) -> Result<Program, ModuleError> {
141    syntax::parse_program_with_file_id(module.file_id, &module.source_text).map_err(|error| {
142        ModuleError::Parse {
143            module: module.module_path.clone(),
144            source: module.source_name.clone(),
145            error,
146        }
147    })
148}
149
150/// Load one imported module source using either bundled or filesystem lookup.
151fn load_import_module(
152    import: &ImportDecl,
153    from: &LoadedModule,
154    mode: ResolveMode,
155    root_dir: &Path,
156    next_file_id: &mut u32,
157) -> Result<LoadedModule, ModuleError> {
158    let import_path = import.path.as_str();
159    if let Some(source_text) = bundled_std_module_source(import_path) {
160        let source_name = if import_path == STD_MODULE_NAME {
161            "<std>/std.rk".to_owned()
162        } else {
163            format!("<std>/{}.rk", import_path.replace("::", "/"))
164        };
165        return Ok(LoadedModule {
166            module_path: import_path.to_owned(),
167            source_name,
168            source_text: source_text.to_owned(),
169            file_id: allocate_file_id(next_file_id),
170            is_entry: false,
171        });
172    }
173
174    if mode == ResolveMode::Browser {
175        let allowed =
176            if from.module_path == STD_MODULE_NAME || from.module_path.starts_with("std::") {
177                "std and std::*"
178            } else {
179                STD_MODULE_NAME
180            };
181        return Err(ModuleError::BrowserStaticImportUnsupported {
182            allowed: allowed.to_owned(),
183            found: import_path.to_owned(),
184            source_name: from.source_name.clone(),
185            span: import.span,
186        });
187    }
188
189    let rel = format!("{}.rk", import_path.replace("::", "/"));
190    let file_path = root_dir.join(&rel);
191    if !file_path.exists() {
192        return Err(ModuleError::NotFound {
193            module: import_path.to_owned(),
194            from: from.module_path.clone(),
195            path: file_path.display().to_string(),
196            source_name: from.source_name.clone(),
197            span: import.span,
198        });
199    }
200
201    let source_text = std::fs::read_to_string(&file_path).map_err(|error| ModuleError::Io {
202        path: file_path.display().to_string(),
203        error,
204    })?;
205    Ok(LoadedModule {
206        module_path: import_path.to_owned(),
207        source_name: file_path.display().to_string(),
208        source_text,
209        file_id: allocate_file_id(next_file_id),
210        is_entry: false,
211    })
212}
213
214/// Validate that one module does not repeat the same import path.
215fn ensure_no_duplicate_imports(
216    module: &LoadedModule,
217    imports: &[ImportDecl],
218) -> Result<(), ModuleError> {
219    let mut seen = BTreeMap::<String, SourceSpan>::new();
220    for import in imports {
221        if let Some(first_span) = seen.get(import.path.as_str()) {
222            return Err(ModuleError::DuplicateImport {
223                module: module.module_path.clone(),
224                import: import.path.clone(),
225                source_name: module.source_name.clone(),
226                first_span: *first_span,
227                duplicate_span: import.span,
228            });
229        }
230        let _ = seen.insert(import.path.clone(), import.span);
231    }
232    Ok(())
233}
234
235/// Merge parsed modules into one combined program.
236fn merge_loaded_modules(modules: &[LoadedModule]) -> Result<Program, ModuleError> {
237    let mut merged = Program {
238        imports: Vec::new(),
239        functions: Vec::new(),
240        meta_functions: Vec::new(),
241        extern_modules: Vec::new(),
242        structs: Vec::new(),
243        enums: Vec::new(),
244    };
245
246    for module in modules {
247        let mut parsed = parse_module(module)?;
248        if module.is_entry {
249            parsed.imports.clear();
250            merged.functions.append(&mut parsed.functions);
251            merged.meta_functions.append(&mut parsed.meta_functions);
252            merged.extern_modules.append(&mut parsed.extern_modules);
253            merged.structs.append(&mut parsed.structs);
254            merged.enums.append(&mut parsed.enums);
255            continue;
256        }
257
258        qualify::qualify_program_module(&mut parsed, module.module_path.as_str());
259        parsed.imports.clear();
260        merged.functions.append(&mut parsed.functions);
261        merged.meta_functions.append(&mut parsed.meta_functions);
262        merged.extern_modules.append(&mut parsed.extern_modules);
263        merged.structs.append(&mut parsed.structs);
264        merged.enums.append(&mut parsed.enums);
265    }
266
267    Ok(merged)
268}
269
270#[cfg(test)]
271mod tests;