Skip to main content

lust/modules/
mod.rs

1use crate::ast::Item;
2#[cfg(feature = "std")]
3use crate::{
4    ast::{FunctionDef, ItemKind, UseTree, Visibility},
5    error::{LustError, Result},
6    lexer::Lexer,
7    parser::Parser,
8    Span,
9};
10use alloc::{string::String, vec::Vec};
11#[cfg(feature = "std")]
12use alloc::{format, vec};
13use hashbrown::{HashMap, HashSet};
14#[cfg(feature = "std")]
15use std::{
16    fs,
17    path::{Path, PathBuf},
18};
19#[derive(Debug, Clone, Default)]
20pub struct ModuleImports {
21    pub function_aliases: HashMap<String, String>,
22    pub module_aliases: HashMap<String, String>,
23    pub type_aliases: HashMap<String, String>,
24}
25
26#[derive(Debug, Clone, Default)]
27pub struct ModuleExports {
28    pub functions: HashMap<String, String>,
29    pub types: HashMap<String, String>,
30}
31
32pub mod embedded;
33
34#[derive(Debug, Clone)]
35pub struct LoadedModule {
36    pub path: String,
37    pub items: Vec<Item>,
38    pub imports: ModuleImports,
39    pub exports: ModuleExports,
40    pub init_function: Option<String>,
41    #[cfg(feature = "std")]
42    pub source_path: PathBuf,
43}
44
45#[derive(Debug, Clone)]
46pub struct Program {
47    pub modules: Vec<LoadedModule>,
48    pub entry_module: String,
49}
50
51pub use embedded::{build_directory_map, load_program_from_embedded, EmbeddedModule};
52#[allow(dead_code)]
53#[derive(Clone, Copy, Debug, Default)]
54struct ImportResolution {
55    import_value: bool,
56    import_type: bool,
57}
58
59impl ImportResolution {
60    fn both() -> Self {
61        Self {
62            import_value: true,
63            import_type: true,
64        }
65    }
66}
67
68#[cfg(feature = "std")]
69pub struct ModuleLoader {
70    base_dir: PathBuf,
71    cache: HashMap<String, LoadedModule>,
72    visited: HashSet<String>,
73    source_overrides: HashMap<PathBuf, String>,
74    module_roots: HashMap<String, Vec<ModuleRoot>>,
75}
76
77#[cfg(feature = "std")]
78#[derive(Debug, Clone)]
79struct ModuleRoot {
80    base: PathBuf,
81    root_module: Option<PathBuf>,
82}
83
84#[cfg(feature = "std")]
85impl ModuleLoader {
86    pub fn new(base_dir: impl Into<PathBuf>) -> Self {
87        Self {
88            base_dir: base_dir.into(),
89            cache: HashMap::new(),
90            visited: HashSet::new(),
91            source_overrides: HashMap::new(),
92            module_roots: HashMap::new(),
93        }
94    }
95
96    pub fn set_source_overrides(&mut self, overrides: HashMap<PathBuf, String>) {
97        self.source_overrides = overrides;
98    }
99
100    pub fn set_source_override<P: Into<PathBuf>, S: Into<String>>(&mut self, path: P, source: S) {
101        self.source_overrides.insert(path.into(), source.into());
102    }
103
104    pub fn clear_source_overrides(&mut self) {
105        self.source_overrides.clear();
106    }
107
108    pub fn add_module_root(
109        &mut self,
110        prefix: impl Into<String>,
111        root: impl Into<PathBuf>,
112        root_module: Option<PathBuf>,
113    ) {
114        self.module_roots
115            .entry(prefix.into())
116            .or_default()
117            .push(ModuleRoot {
118                base: root.into(),
119                root_module,
120            });
121    }
122
123    pub fn load_program_from_entry(&mut self, entry_file: &str) -> Result<Program> {
124        let entry_path = Path::new(entry_file);
125        let entry_dir = entry_path.parent().unwrap_or_else(|| Path::new("."));
126        self.base_dir = entry_dir.to_path_buf();
127        let entry_module = Self::module_path_for_file(entry_path);
128        let mut order: Vec<String> = Vec::new();
129        let mut stack: HashSet<String> = HashSet::new();
130        self.load_module_recursive(&entry_module, &mut order, &mut stack, true)?;
131        let modules = order
132            .into_iter()
133            .filter_map(|m| self.cache.get(&m).cloned())
134            .collect::<Vec<_>>();
135        Ok(Program {
136            modules,
137            entry_module,
138        })
139    }
140
141    fn load_module_recursive(
142        &mut self,
143        module_path: &str,
144        order: &mut Vec<String>,
145        stack: &mut HashSet<String>,
146        is_entry: bool,
147    ) -> Result<()> {
148        if self.visited.contains(module_path) {
149            return Ok(());
150        }
151
152        if !stack.insert(module_path.to_string()) {
153            return Ok(());
154        }
155
156        let mut loaded = self.load_single_module(module_path, is_entry)?;
157        self.cache.insert(module_path.to_string(), loaded.clone());
158        let deps = self.collect_dependencies(&loaded.items);
159        for dep in deps {
160            self.load_module_recursive(&dep, order, stack, false)?;
161        }
162
163        self.finalize_module(&mut loaded)?;
164        self.cache.insert(module_path.to_string(), loaded.clone());
165        self.visited.insert(module_path.to_string());
166        order.push(module_path.to_string());
167        stack.remove(module_path);
168        Ok(())
169    }
170
171    fn load_single_module(&self, module_path: &str, is_entry: bool) -> Result<LoadedModule> {
172        let file = self.file_for_module_path(module_path);
173        let source = if let Some(src) = self.source_overrides.get(&file) {
174            src.clone()
175        } else {
176            match fs::read_to_string(&file) {
177                Ok(src) => src,
178                Err(e) => {
179                    // For non-entry modules, allow missing files and create stub
180                    if !is_entry && e.kind() == std::io::ErrorKind::NotFound {
181                        #[cfg(feature = "std")]
182                        eprintln!(
183                            "[WARNING] Module '{}' not found, but present in code",
184                            module_path
185                        );
186                        //return Ok(self.create_nil_stub_module(module_path));
187                    }
188                    return Err(LustError::Unknown(format!(
189                        "Failed to read module '{}': {}",
190                        file.display(),
191                        e
192                    )));
193                }
194            }
195        };
196        let mut lexer = Lexer::new(&source);
197        let tokens = lexer
198            .tokenize()
199            .map_err(|err| Self::attach_module_to_error(err, module_path))?;
200        let mut parser = Parser::new(tokens);
201        let mut items = parser
202            .parse()
203            .map_err(|err| Self::attach_module_to_error(err, module_path))?;
204        let mut imports = ModuleImports::default();
205        let mut exports = ModuleExports::default();
206        let mut new_items: Vec<Item> = Vec::new();
207        let mut init_function: Option<String> = None;
208        let mut pending_init_stmts: Vec<crate::ast::Stmt> = Vec::new();
209        let mut pending_init_span: Option<Span> = None;
210        for item in items.drain(..) {
211            match &item.kind {
212                ItemKind::Function(func) => {
213                    let mut f = func.clone();
214                    if !f.is_method && !f.name.contains(':') && !f.name.contains('.') {
215                        let fq = format!("{}.{}", module_path, f.name);
216                        imports.function_aliases.insert(f.name.clone(), fq.clone());
217                        f.name = fq.clone();
218                        if matches!(f.visibility, Visibility::Public) {
219                            exports
220                                .functions
221                                .insert(self.simple_name(&f.name).to_string(), f.name.clone());
222                        }
223                    } else {
224                        if matches!(f.visibility, Visibility::Public) {
225                            exports
226                                .functions
227                                .insert(self.simple_name(&f.name).to_string(), f.name.clone());
228                        }
229                    }
230
231                    new_items.push(Item::new(ItemKind::Function(f), item.span));
232                }
233
234                ItemKind::Struct(s) => {
235                    if matches!(s.visibility, Visibility::Public) {
236                        exports
237                            .types
238                            .insert(s.name.clone(), format!("{}.{}", module_path, s.name));
239                    }
240
241                    new_items.push(item);
242                }
243
244                ItemKind::Enum(e) => {
245                    if matches!(e.visibility, Visibility::Public) {
246                        exports
247                            .types
248                            .insert(e.name.clone(), format!("{}.{}", module_path, e.name));
249                    }
250
251                    new_items.push(item);
252                }
253
254                ItemKind::Trait(t) => {
255                    if matches!(t.visibility, Visibility::Public) {
256                        exports
257                            .types
258                            .insert(t.name.clone(), format!("{}.{}", module_path, t.name));
259                    }
260
261                    new_items.push(item);
262                }
263
264                ItemKind::TypeAlias { name, .. } => {
265                    exports
266                        .types
267                        .insert(name.clone(), format!("{}.{}", module_path, name));
268                    new_items.push(item);
269                }
270
271                ItemKind::Script(stmts) => {
272                    if is_entry {
273                        new_items.push(Item::new(ItemKind::Script(stmts.clone()), item.span));
274                    } else {
275                        if pending_init_span.is_none() {
276                            pending_init_span = Some(item.span);
277                        }
278                        pending_init_stmts.extend(stmts.iter().cloned());
279                    }
280                }
281
282                ItemKind::Extern {
283                    abi,
284                    items: extern_items,
285                } => {
286                    let mut rewritten = Vec::new();
287                    for extern_item in extern_items {
288                        match extern_item {
289                            crate::ast::ExternItem::Function {
290                                name,
291                                params,
292                                return_type,
293                            } => {
294                                let mut new_name = name.clone();
295                                if let Some((head, tail)) = new_name.split_once(':') {
296                                    let qualified_head =
297                                        if head.contains('.') || head.contains("::") {
298                                            head.to_string()
299                                        } else {
300                                            format!("{}.{}", module_path, head)
301                                        };
302                                    new_name = format!("{}:{}", qualified_head, tail);
303                                } else if !new_name.contains('.') {
304                                    new_name = format!("{}.{}", module_path, new_name);
305                                }
306
307                                exports.functions.insert(
308                                    self.simple_name(&new_name).to_string(),
309                                    new_name.clone(),
310                                );
311                                imports.function_aliases.insert(
312                                    self.simple_name(&new_name).to_string(),
313                                    new_name.clone(),
314                                );
315
316                                rewritten.push(crate::ast::ExternItem::Function {
317                                    name: new_name,
318                                    params: params.clone(),
319                                    return_type: return_type.clone(),
320                                });
321                            }
322
323                            crate::ast::ExternItem::Const { name, ty } => {
324                                let qualified = if name.contains('.') {
325                                    name.clone()
326                                } else {
327                                    format!("{}.{}", module_path, name)
328                                };
329                                exports.functions.insert(
330                                    self.simple_name(&qualified).to_string(),
331                                    qualified.clone(),
332                                );
333                                imports.function_aliases.insert(
334                                    self.simple_name(&qualified).to_string(),
335                                    qualified.clone(),
336                                );
337                                rewritten.push(crate::ast::ExternItem::Const {
338                                    name: qualified,
339                                    ty: ty.clone(),
340                                });
341                            }
342
343                            crate::ast::ExternItem::Struct(def) => {
344                                let mut def = def.clone();
345                                if !def.name.contains('.') && !def.name.contains("::") {
346                                    def.name = format!("{}.{}", module_path, def.name);
347                                }
348                                exports.types.insert(
349                                    self.simple_name(&def.name).to_string(),
350                                    def.name.clone(),
351                                );
352                                rewritten.push(crate::ast::ExternItem::Struct(def));
353                            }
354
355                            crate::ast::ExternItem::Enum(def) => {
356                                let mut def = def.clone();
357                                if !def.name.contains('.') && !def.name.contains("::") {
358                                    def.name = format!("{}.{}", module_path, def.name);
359                                }
360                                exports.types.insert(
361                                    self.simple_name(&def.name).to_string(),
362                                    def.name.clone(),
363                                );
364                                rewritten.push(crate::ast::ExternItem::Enum(def));
365                            }
366                        }
367                    }
368                    new_items.push(Item::new(
369                        ItemKind::Extern {
370                            abi: abi.clone(),
371                            items: rewritten,
372                        },
373                        item.span,
374                    ));
375                }
376
377                _ => {
378                    new_items.push(item);
379                }
380            }
381        }
382
383        if !is_entry && !pending_init_stmts.is_empty() {
384            let init_name = format!("__init@{}", module_path);
385            let func = FunctionDef {
386                name: init_name.clone(),
387                type_params: vec![],
388                trait_bounds: vec![],
389                params: vec![],
390                return_type: None,
391                body: pending_init_stmts,
392                is_method: false,
393                visibility: Visibility::Private,
394            };
395            let span = pending_init_span.unwrap_or_else(Span::dummy);
396            // Place module init first so the compiler can observe module-level locals
397            // (e.g. transpiler prelude helpers) before compiling functions that reference them.
398            new_items.insert(0, Item::new(ItemKind::Function(func), span));
399            init_function = Some(init_name);
400        }
401
402        Ok(LoadedModule {
403            path: module_path.to_string(),
404            items: new_items,
405            imports,
406            exports,
407            init_function,
408            source_path: file,
409        })
410    }
411
412    fn collect_dependencies(&self, items: &[Item]) -> Vec<String> {
413        let mut deps = HashSet::new();
414        for item in items {
415            match &item.kind {
416                ItemKind::Use { public: _, tree } => {
417                    self.collect_deps_from_use(tree, &mut deps);
418                }
419                ItemKind::Script(stmts) => {
420                    for stmt in stmts {
421                        self.collect_deps_from_lua_require_stmt(stmt, &mut deps);
422                    }
423                }
424                ItemKind::Function(func) => {
425                    for stmt in &func.body {
426                        self.collect_deps_from_lua_require_stmt(stmt, &mut deps);
427                    }
428                }
429                ItemKind::Const { value, .. } | ItemKind::Static { value, .. } => {
430                    self.collect_deps_from_lua_require_expr(value, &mut deps);
431                }
432                ItemKind::Impl(impl_block) => {
433                    for method in &impl_block.methods {
434                        for stmt in &method.body {
435                            self.collect_deps_from_lua_require_stmt(stmt, &mut deps);
436                        }
437                    }
438                }
439                ItemKind::Trait(trait_def) => {
440                    for method in &trait_def.methods {
441                        if let Some(default_impl) = &method.default_impl {
442                            for stmt in default_impl {
443                                self.collect_deps_from_lua_require_stmt(stmt, &mut deps);
444                            }
445                        }
446                    }
447                }
448                _ => {}
449            }
450        }
451
452        deps.into_iter().collect()
453    }
454
455    fn collect_deps_from_lua_require_stmt(
456        &self,
457        stmt: &crate::ast::Stmt,
458        deps: &mut HashSet<String>,
459    ) {
460        use crate::ast::StmtKind;
461        match &stmt.kind {
462            StmtKind::Local { initializer, .. } => {
463                if let Some(values) = initializer {
464                    for expr in values {
465                        self.collect_deps_from_lua_require_expr(expr, deps);
466                    }
467                }
468            }
469            StmtKind::Assign { targets, values } => {
470                for expr in targets {
471                    self.collect_deps_from_lua_require_expr(expr, deps);
472                }
473                for expr in values {
474                    self.collect_deps_from_lua_require_expr(expr, deps);
475                }
476            }
477            StmtKind::CompoundAssign { target, value, .. } => {
478                self.collect_deps_from_lua_require_expr(target, deps);
479                self.collect_deps_from_lua_require_expr(value, deps);
480            }
481            StmtKind::Expr(expr) => self.collect_deps_from_lua_require_expr(expr, deps),
482            StmtKind::If {
483                condition,
484                then_block,
485                elseif_branches,
486                else_block,
487            } => {
488                self.collect_deps_from_lua_require_expr(condition, deps);
489                for stmt in then_block {
490                    self.collect_deps_from_lua_require_stmt(stmt, deps);
491                }
492                for (cond, block) in elseif_branches {
493                    self.collect_deps_from_lua_require_expr(cond, deps);
494                    for stmt in block {
495                        self.collect_deps_from_lua_require_stmt(stmt, deps);
496                    }
497                }
498                if let Some(block) = else_block {
499                    for stmt in block {
500                        self.collect_deps_from_lua_require_stmt(stmt, deps);
501                    }
502                }
503            }
504            StmtKind::While { condition, body } => {
505                self.collect_deps_from_lua_require_expr(condition, deps);
506                for stmt in body {
507                    self.collect_deps_from_lua_require_stmt(stmt, deps);
508                }
509            }
510            StmtKind::ForNumeric {
511                start,
512                end,
513                step,
514                body,
515                ..
516            } => {
517                self.collect_deps_from_lua_require_expr(start, deps);
518                self.collect_deps_from_lua_require_expr(end, deps);
519                if let Some(step) = step {
520                    self.collect_deps_from_lua_require_expr(step, deps);
521                }
522                for stmt in body {
523                    self.collect_deps_from_lua_require_stmt(stmt, deps);
524                }
525            }
526            StmtKind::ForIn { iterator, body, .. } => {
527                self.collect_deps_from_lua_require_expr(iterator, deps);
528                for stmt in body {
529                    self.collect_deps_from_lua_require_stmt(stmt, deps);
530                }
531            }
532            StmtKind::Return(values) => {
533                for expr in values {
534                    self.collect_deps_from_lua_require_expr(expr, deps);
535                }
536            }
537            StmtKind::Block(stmts) => {
538                for stmt in stmts {
539                    self.collect_deps_from_lua_require_stmt(stmt, deps);
540                }
541            }
542            StmtKind::Break | StmtKind::Continue => {}
543        }
544    }
545
546    fn collect_deps_from_lua_require_expr(
547        &self,
548        expr: &crate::ast::Expr,
549        deps: &mut HashSet<String>,
550    ) {
551        use crate::ast::{ExprKind, Literal};
552        match &expr.kind {
553            ExprKind::Call { callee, args } => {
554                if self.is_lua_require_callee(callee) {
555                    if let Some(name) = args
556                        .get(0)
557                        .and_then(|arg| self.extract_lua_require_name(arg))
558                    {
559                        if !Self::is_lua_builtin_module_name(&name) {
560                            // `lua.require()` calls originate from transpiled Lua stubs. Unlike
561                            // Lust `use` imports, these should only pull in modules that we can
562                            // actually locate in the current module roots (extern stubs, on-disk
563                            // modules, or source overrides). This prevents optional Lua requires
564                            // (e.g. `ssl.https`) from becoming hard compile-time dependencies.
565                            let file = self.file_for_module_path(&name);
566                            if self.module_source_known(&name, &file) {
567                                deps.insert(name);
568                            }
569                        }
570                    }
571                }
572                self.collect_deps_from_lua_require_expr(callee, deps);
573                for arg in args {
574                    self.collect_deps_from_lua_require_expr(arg, deps);
575                }
576            }
577            ExprKind::MethodCall { receiver, args, .. } => {
578                self.collect_deps_from_lua_require_expr(receiver, deps);
579                for arg in args {
580                    self.collect_deps_from_lua_require_expr(arg, deps);
581                }
582            }
583            ExprKind::Binary { left, right, .. } => {
584                self.collect_deps_from_lua_require_expr(left, deps);
585                self.collect_deps_from_lua_require_expr(right, deps);
586            }
587            ExprKind::Unary { operand, .. } => {
588                self.collect_deps_from_lua_require_expr(operand, deps)
589            }
590            ExprKind::FieldAccess { object, .. } => {
591                self.collect_deps_from_lua_require_expr(object, deps)
592            }
593            ExprKind::Index { object, index } => {
594                self.collect_deps_from_lua_require_expr(object, deps);
595                self.collect_deps_from_lua_require_expr(index, deps);
596            }
597            ExprKind::Array(elements) | ExprKind::Tuple(elements) => {
598                for element in elements {
599                    self.collect_deps_from_lua_require_expr(element, deps);
600                }
601            }
602            ExprKind::Map(entries) => {
603                for (k, v) in entries {
604                    self.collect_deps_from_lua_require_expr(k, deps);
605                    self.collect_deps_from_lua_require_expr(v, deps);
606                }
607            }
608            ExprKind::StructLiteral { fields, .. } => {
609                for field in fields {
610                    self.collect_deps_from_lua_require_expr(&field.value, deps);
611                }
612            }
613            ExprKind::EnumConstructor { args, .. } => {
614                for arg in args {
615                    self.collect_deps_from_lua_require_expr(arg, deps);
616                }
617            }
618            ExprKind::Lambda { body, .. } => self.collect_deps_from_lua_require_expr(body, deps),
619            ExprKind::Paren(inner) => self.collect_deps_from_lua_require_expr(inner, deps),
620            ExprKind::Cast { expr, .. } => self.collect_deps_from_lua_require_expr(expr, deps),
621            ExprKind::TypeCheck { expr, .. } => self.collect_deps_from_lua_require_expr(expr, deps),
622            ExprKind::IsPattern { expr, .. } => self.collect_deps_from_lua_require_expr(expr, deps),
623            ExprKind::If {
624                condition,
625                then_branch,
626                else_branch,
627            } => {
628                self.collect_deps_from_lua_require_expr(condition, deps);
629                self.collect_deps_from_lua_require_expr(then_branch, deps);
630                if let Some(other) = else_branch {
631                    self.collect_deps_from_lua_require_expr(other, deps);
632                }
633            }
634            ExprKind::Block(stmts) => {
635                for stmt in stmts {
636                    self.collect_deps_from_lua_require_stmt(stmt, deps);
637                }
638            }
639            ExprKind::Return(values) => {
640                for value in values {
641                    self.collect_deps_from_lua_require_expr(value, deps);
642                }
643            }
644            ExprKind::Range { start, end, .. } => {
645                self.collect_deps_from_lua_require_expr(start, deps);
646                self.collect_deps_from_lua_require_expr(end, deps);
647            }
648            ExprKind::Literal(Literal::String(_))
649            | ExprKind::Literal(_)
650            | ExprKind::Identifier(_) => {}
651        }
652    }
653
654    fn is_lua_builtin_module_name(name: &str) -> bool {
655        matches!(
656            name,
657            "math" | "table" | "string" | "io" | "os" | "package" | "coroutine" | "debug" | "utf8"
658        )
659    }
660
661    fn is_lua_require_callee(&self, callee: &crate::ast::Expr) -> bool {
662        use crate::ast::ExprKind;
663        match &callee.kind {
664            ExprKind::Identifier(name) => name == "require",
665            ExprKind::FieldAccess { object, field } => {
666                field == "require"
667                    && matches!(&object.kind, ExprKind::Identifier(name) if name == "lua")
668            }
669            _ => false,
670        }
671    }
672
673    fn extract_lua_require_name(&self, expr: &crate::ast::Expr) -> Option<String> {
674        use crate::ast::{ExprKind, Literal};
675        match &expr.kind {
676            ExprKind::Literal(Literal::String(s)) => Some(s.clone()),
677            ExprKind::Call { callee, args } if self.is_lua_to_value_callee(callee) => {
678                args.get(0).and_then(|arg| match &arg.kind {
679                    ExprKind::Literal(Literal::String(s)) => Some(s.clone()),
680                    _ => None,
681                })
682            }
683            _ => None,
684        }
685    }
686
687    fn is_lua_to_value_callee(&self, callee: &crate::ast::Expr) -> bool {
688        use crate::ast::ExprKind;
689        matches!(
690            &callee.kind,
691            ExprKind::FieldAccess { object, field }
692                if field == "to_value"
693                    && matches!(&object.kind, ExprKind::Identifier(name) if name == "lua")
694        )
695    }
696
697    fn finalize_module(&mut self, module: &mut LoadedModule) -> Result<()> {
698        for item in &module.items {
699            if let ItemKind::Use { tree, .. } = &item.kind {
700                self.process_use_tree(tree, &mut module.imports)?;
701            }
702        }
703
704        for item in &module.items {
705            if let ItemKind::Use { public: true, tree } = &item.kind {
706                self.apply_reexport(tree, &mut module.exports)?;
707            }
708        }
709
710        module
711            .imports
712            .module_aliases
713            .entry(self.simple_tail(&module.path).to_string())
714            .or_insert_with(|| module.path.clone());
715        Ok(())
716    }
717
718    fn collect_deps_from_use(&self, tree: &UseTree, deps: &mut HashSet<String>) {
719        match tree {
720            UseTree::Path {
721                path,
722                alias: _,
723                import_module: _,
724            } => {
725                let full = path.join(".");
726                let full_file = self.file_for_module_path(&full);
727                if self.module_source_known(&full, &full_file) {
728                    deps.insert(full);
729                } else if path.len() > 1 {
730                    deps.insert(path[..path.len() - 1].join("."));
731                }
732            }
733
734            UseTree::Group { prefix, items } => {
735                let module = prefix.join(".");
736                if !module.is_empty() {
737                    deps.insert(module);
738                }
739
740                for item in items {
741                    if item.path.len() > 1 {
742                        let mut combined: Vec<String> = prefix.clone();
743                        combined.extend(item.path[..item.path.len() - 1].iter().cloned());
744                        let module_path = combined.join(".");
745                        if !module_path.is_empty() {
746                            deps.insert(module_path);
747                        }
748                    }
749                }
750            }
751
752            UseTree::Glob { prefix } => {
753                deps.insert(prefix.join("."));
754            }
755        }
756    }
757
758    fn process_use_tree(&self, tree: &UseTree, imports: &mut ModuleImports) -> Result<()> {
759        match tree {
760            UseTree::Path { path, alias, .. } => {
761                let full = path.join(".");
762                let full_file = self.file_for_module_path(&full);
763                if self.module_source_known(&full, &full_file) {
764                    let alias_name = alias
765                        .clone()
766                        .unwrap_or_else(|| path.last().unwrap().clone());
767                    imports.module_aliases.insert(alias_name, full);
768                } else if path.len() > 1 {
769                    let module = path[..path.len() - 1].join(".");
770                    let item = path.last().unwrap().clone();
771                    let alias_name = alias.clone().unwrap_or_else(|| item.clone());
772                    let classification = self.classify_import_target(&module, &item);
773                    let fq = format!("{}.{}", module, item);
774                    if classification.import_value {
775                        imports
776                            .function_aliases
777                            .insert(alias_name.clone(), fq.clone());
778                    }
779
780                    if classification.import_type {
781                        imports.type_aliases.insert(alias_name, fq);
782                    }
783                }
784            }
785
786            UseTree::Group { prefix, items } => {
787                for item in items {
788                    if item.path.is_empty() {
789                        continue;
790                    }
791
792                    let alias_name = item
793                        .alias
794                        .clone()
795                        .unwrap_or_else(|| item.path.last().unwrap().clone());
796                    let mut full_segments = prefix.clone();
797                    full_segments.extend(item.path.clone());
798                    let full = full_segments.join(".");
799                    let full_file = self.file_for_module_path(&full);
800                    if self.module_source_known(&full, &full_file) {
801                        imports.module_aliases.insert(alias_name, full);
802                        continue;
803                    }
804
805                    let mut module_segments = full_segments.clone();
806                    let item_name = module_segments.pop().unwrap();
807                    let module_path = module_segments.join(".");
808                    let fq_name = if module_path.is_empty() {
809                        item_name.clone()
810                    } else {
811                        format!("{}.{}", module_path, item_name)
812                    };
813                    let classification = self.classify_import_target(&module_path, &item_name);
814                    if classification.import_value {
815                        imports
816                            .function_aliases
817                            .insert(alias_name.clone(), fq_name.clone());
818                    }
819
820                    if classification.import_type {
821                        imports.type_aliases.insert(alias_name.clone(), fq_name);
822                    }
823                }
824            }
825
826            UseTree::Glob { prefix } => {
827                let module = prefix.join(".");
828                if let Some(loaded) = self.cache.get(&module) {
829                    for (name, fq) in &loaded.exports.functions {
830                        imports.function_aliases.insert(name.clone(), fq.clone());
831                    }
832
833                    for (name, fq) in &loaded.exports.types {
834                        imports.type_aliases.insert(name.clone(), fq.clone());
835                    }
836                }
837
838                let alias_name = prefix.last().cloned().unwrap_or_else(|| module.clone());
839                if !module.is_empty() {
840                    imports.module_aliases.insert(alias_name, module);
841                }
842            }
843        }
844
845        Ok(())
846    }
847
848    fn attach_module_to_error(error: LustError, module_path: &str) -> LustError {
849        match error {
850            LustError::LexerError {
851                line,
852                column,
853                message,
854                module,
855            } => LustError::LexerError {
856                line,
857                column,
858                message,
859                module: module.or_else(|| Some(module_path.to_string())),
860            },
861            LustError::ParserError {
862                line,
863                column,
864                message,
865                module,
866            } => LustError::ParserError {
867                line,
868                column,
869                message,
870                module: module.or_else(|| Some(module_path.to_string())),
871            },
872            LustError::CompileErrorWithSpan {
873                message,
874                line,
875                column,
876                module,
877            } => LustError::CompileErrorWithSpan {
878                message,
879                line,
880                column,
881                module: module.or_else(|| Some(module_path.to_string())),
882            },
883            other => other,
884        }
885    }
886
887    fn apply_reexport(&self, tree: &UseTree, exports: &mut ModuleExports) -> Result<()> {
888        match tree {
889            UseTree::Path { path, alias, .. } => {
890                if path.len() == 1 {
891                    return Ok(());
892                }
893
894                let module = path[..path.len() - 1].join(".");
895                let item = path.last().unwrap().clone();
896                let alias_name = alias.clone().unwrap_or_else(|| item.clone());
897                let fq = format!("{}.{}", module, item);
898                let classification = self.classify_import_target(&module, &item);
899                if classification.import_type {
900                    exports.types.insert(alias_name.clone(), fq.clone());
901                }
902
903                if classification.import_value {
904                    exports.functions.insert(alias_name, fq);
905                }
906
907                Ok(())
908            }
909
910            UseTree::Group { prefix, items } => {
911                for item in items {
912                    if item.path.is_empty() {
913                        continue;
914                    }
915
916                    let alias_name = item
917                        .alias
918                        .clone()
919                        .unwrap_or_else(|| item.path.last().unwrap().clone());
920                    let mut full_segments = prefix.clone();
921                    full_segments.extend(item.path.clone());
922                    let full = full_segments.join(".");
923                    let full_file = self.file_for_module_path(&full);
924                    if self.module_source_known(&full, &full_file) {
925                        continue;
926                    }
927
928                    let mut module_segments = full_segments.clone();
929                    let item_name = module_segments.pop().unwrap();
930                    let module_path = module_segments.join(".");
931                    let fq_name = if module_path.is_empty() {
932                        item_name.clone()
933                    } else {
934                        format!("{}.{}", module_path, item_name)
935                    };
936                    let classification = self.classify_import_target(&module_path, &item_name);
937                    if classification.import_type {
938                        exports.types.insert(alias_name.clone(), fq_name.clone());
939                    }
940
941                    if classification.import_value {
942                        exports.functions.insert(alias_name.clone(), fq_name);
943                    }
944                }
945
946                Ok(())
947            }
948
949            UseTree::Glob { prefix } => {
950                let module = prefix.join(".");
951                if let Some(loaded) = self.cache.get(&module) {
952                    for (n, fq) in &loaded.exports.types {
953                        exports.types.insert(n.clone(), fq.clone());
954                    }
955
956                    for (n, fq) in &loaded.exports.functions {
957                        exports.functions.insert(n.clone(), fq.clone());
958                    }
959                }
960
961                Ok(())
962            }
963        }
964    }
965
966    fn simple_name<'a>(&self, qualified: &'a str) -> &'a str {
967        qualified
968            .rsplit_once('.')
969            .map(|(_, n)| n)
970            .unwrap_or(qualified)
971    }
972
973    fn simple_tail<'a>(&self, module_path: &'a str) -> &'a str {
974        module_path
975            .rsplit_once('.')
976            .map(|(_, n)| n)
977            .unwrap_or(module_path)
978    }
979
980    fn module_source_known(&self, module_path: &str, file: &Path) -> bool {
981        file.exists()
982            || self.source_overrides.contains_key(file)
983            || self.cache.contains_key(module_path)
984    }
985
986    fn classify_import_target(&self, module_path: &str, item_name: &str) -> ImportResolution {
987        if module_path.is_empty() {
988            return ImportResolution::both();
989        }
990
991        if let Some(module) = self.cache.get(module_path) {
992            let has_value = module.exports.functions.contains_key(item_name);
993            let has_type = module.exports.types.contains_key(item_name);
994            if has_value || has_type {
995                return ImportResolution {
996                    import_value: has_value,
997                    import_type: has_type,
998                };
999            }
1000        }
1001
1002        ImportResolution::both()
1003    }
1004
1005    fn file_for_module_path(&self, module_path: &str) -> PathBuf {
1006        let segments: Vec<&str> = module_path.split('.').collect();
1007        let candidates = self.resolve_dependency_roots(&segments);
1008        if !candidates.is_empty() {
1009            let mut fallback: Option<PathBuf> = None;
1010            for (root, consumed) in candidates.iter().rev() {
1011                let candidate = Self::path_from_root(root, &segments, *consumed);
1012                if candidate.exists() {
1013                    return candidate;
1014                }
1015                if fallback.is_none() {
1016                    fallback = Some(candidate);
1017                }
1018            }
1019            if let Some(path) = fallback {
1020                return path;
1021            }
1022        }
1023
1024        let mut fallback = self.base_dir.clone();
1025        for seg in &segments {
1026            fallback.push(seg);
1027        }
1028        fallback.set_extension("lust");
1029        fallback
1030    }
1031
1032    fn resolve_dependency_roots(&self, segments: &[&str]) -> Vec<(&ModuleRoot, usize)> {
1033        let mut matched: Vec<(&ModuleRoot, usize)> = Vec::new();
1034        let mut prefix_segments: Vec<&str> = Vec::new();
1035        for (index, segment) in segments.iter().enumerate() {
1036            prefix_segments.push(*segment);
1037            let key = prefix_segments.join(".");
1038            if let Some(roots) = self.module_roots.get(&key) {
1039                for root in roots {
1040                    matched.push((root, index + 1));
1041                }
1042            }
1043        }
1044        matched
1045    }
1046
1047    fn path_from_root(root: &ModuleRoot, segments: &[&str], consumed: usize) -> PathBuf {
1048        let mut path = root.base.clone();
1049        if consumed == segments.len() {
1050            if let Some(relative) = &root.root_module {
1051                path.push(relative);
1052            } else if let Some(last) = segments.last() {
1053                path.push(format!("{last}.lust"));
1054            }
1055            return path;
1056        }
1057        for seg in &segments[consumed..segments.len() - 1] {
1058            path.push(seg);
1059        }
1060        if let Some(last) = segments.last() {
1061            path.push(format!("{last}.lust"));
1062        }
1063        path
1064    }
1065
1066    fn module_path_for_file(path: &Path) -> String {
1067        let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
1068        stem.to_string()
1069    }
1070}
1071
1072#[cfg(not(feature = "std"))]
1073pub struct ModuleLoader {
1074    cache: HashMap<String, LoadedModule>,
1075    visited: HashSet<String>,
1076}
1077
1078#[cfg(not(feature = "std"))]
1079impl ModuleLoader {
1080    pub fn new() -> Self {
1081        Self {
1082            cache: HashMap::new(),
1083            visited: HashSet::new(),
1084        }
1085    }
1086
1087    pub fn clear_cache(&mut self) {
1088        self.cache.clear();
1089        self.visited.clear();
1090    }
1091
1092    pub fn load_program_from_modules(
1093        &mut self,
1094        modules: Vec<LoadedModule>,
1095        entry_module: String,
1096    ) -> Program {
1097        Program {
1098            modules,
1099            entry_module,
1100        }
1101    }
1102}
1103
1104#[cfg(test)]
1105mod tests {
1106    use super::*;
1107    use std::fs;
1108    use std::time::{SystemTime, UNIX_EPOCH};
1109
1110    fn unique_temp_dir(prefix: &str) -> std::path::PathBuf {
1111        let nanos = SystemTime::now()
1112            .duration_since(UNIX_EPOCH)
1113            .unwrap_or_default()
1114            .as_nanos();
1115        let mut dir = std::env::temp_dir();
1116        dir.push(format!("{prefix}_{nanos}"));
1117        dir
1118    }
1119
1120    #[test]
1121    fn merges_multiple_script_chunks_into_single_init() {
1122        let dir = unique_temp_dir("lust_module_loader_test");
1123        fs::create_dir_all(&dir).unwrap();
1124        let entry_path = dir.join("main.lust");
1125        let module_path = dir.join("m.lust");
1126
1127        fs::write(&entry_path, "use m as m\n").unwrap();
1128
1129        // Interleaved script + declarations; parser will produce multiple ItemKind::Script chunks.
1130        fs::write(
1131            &module_path,
1132            r#"
1133local a: int = 1
1134
1135pub function f(): int
1136    return a
1137end
1138
1139local b: int = 2
1140
1141pub function g(): int
1142    return b
1143end
1144
1145local c: int = a + b
1146"#,
1147        )
1148        .unwrap();
1149
1150        let mut loader = ModuleLoader::new(dir.clone());
1151        let program = loader
1152            .load_program_from_entry(entry_path.to_str().unwrap())
1153            .unwrap();
1154
1155        let module = program.modules.iter().find(|m| m.path == "m").unwrap();
1156        assert_eq!(module.init_function.as_deref(), Some("__init@m"));
1157
1158        let init_functions: Vec<&FunctionDef> = module
1159            .items
1160            .iter()
1161            .filter_map(|item| match &item.kind {
1162                ItemKind::Function(f) if f.name == "__init@m" => Some(f),
1163                _ => None,
1164            })
1165            .collect();
1166        assert_eq!(init_functions.len(), 1);
1167        assert_eq!(init_functions[0].body.len(), 3);
1168
1169        // Best-effort cleanup.
1170        let _ = fs::remove_file(entry_path);
1171        let _ = fs::remove_file(module_path);
1172        let _ = fs::remove_dir(dir);
1173    }
1174
1175    #[test]
1176    fn lua_require_does_not_force_missing_modules_as_dependencies() {
1177        let dir = unique_temp_dir("lust_module_loader_lua_require_missing");
1178        fs::create_dir_all(&dir).unwrap();
1179        let entry_path = dir.join("main.lust");
1180        let module_path = dir.join("a.lust");
1181
1182        fs::write(&entry_path, "use a as a\n").unwrap();
1183
1184        // `lua.require("missing.module")` appears inside a closure (so it should remain optional).
1185        // The module loader must not try to resolve it as a hard on-disk dependency.
1186        fs::write(
1187            &module_path,
1188            r#"
1189use lua as lua
1190
1191local f = function(): unknown
1192    return lua.require("missing.module")
1193end
1194"#,
1195        )
1196        .unwrap();
1197
1198        let mut loader = ModuleLoader::new(dir.clone());
1199        let program = loader
1200            .load_program_from_entry(entry_path.to_str().unwrap())
1201            .unwrap();
1202
1203        assert!(program.modules.iter().any(|m| m.path == "a"));
1204        assert!(!program.modules.iter().any(|m| m.path == "missing.module"));
1205
1206        // Best-effort cleanup.
1207        let _ = fs::remove_file(entry_path);
1208        let _ = fs::remove_file(module_path);
1209        let _ = fs::remove_dir(dir);
1210    }
1211}