Skip to main content

shape_vm/
module_resolution.rs

1//! Module loading, virtual module resolution, and file-based import handling.
2//!
3//! Methods for resolving imports via virtual modules (extension-bundled sources),
4//! file-based module loaders, and the module loader configuration API.
5
6use crate::configuration::BytecodeExecutor;
7
8use shape_ast::Program;
9use shape_ast::ast::{DestructurePattern, ExportItem, Item};
10use shape_ast::parser::parse_program;
11use shape_runtime::module_loader::ModuleCode;
12
13/// Check whether an AST item's name is in the given set of imported names.
14/// Items without a clear name (Impl, Extend, Import) are always included
15/// because they may be required by the named items.
16fn should_include_item(item: &Item, names: &std::collections::HashSet<&str>) -> bool {
17    match item {
18        Item::Function(func_def, _) => names.contains(func_def.name.as_str()),
19        Item::Export(export, _) => match &export.item {
20            ExportItem::Function(f) => names.contains(f.name.as_str()),
21            ExportItem::Enum(e) => names.contains(e.name.as_str()),
22            ExportItem::Struct(s) => names.contains(s.name.as_str()),
23            ExportItem::Trait(t) => names.contains(t.name.as_str()),
24            ExportItem::TypeAlias(a) => names.contains(a.name.as_str()),
25            ExportItem::Interface(i) => names.contains(i.name.as_str()),
26            ExportItem::ForeignFunction(f) => names.contains(f.name.as_str()),
27            ExportItem::Named(specs) => specs.iter().any(|s| names.contains(s.name.as_str())),
28        },
29        Item::StructType(def, _) => names.contains(def.name.as_str()),
30        Item::Enum(def, _) => names.contains(def.name.as_str()),
31        Item::Trait(def, _) => names.contains(def.name.as_str()),
32        Item::TypeAlias(def, _) => names.contains(def.name.as_str()),
33        Item::Interface(def, _) => names.contains(def.name.as_str()),
34        Item::VariableDecl(decl, _) => {
35            if let DestructurePattern::Identifier(name, _) = &decl.pattern {
36                names.contains(name.as_str())
37            } else {
38                false
39            }
40        }
41        // Always include impl/extend — they implement traits/methods for types
42        Item::Impl(..) | Item::Extend(..) => true,
43        // Always include sub-imports — transitive deps needed by inlined items
44        Item::Import(..) => true,
45        _ => false,
46    }
47}
48
49/// Prepend fully-resolved prelude module AST items into the program.
50///
51/// Loads `std::core::prelude`, parses its import statements to discover which
52/// modules it references, then loads those modules and inlines their AST
53/// definitions into the program. The prelude's own import statements are NOT
54/// included (only the referenced module definitions), so `append_imported_module_items`
55/// will not double-include them.
56///
57/// The resolved prelude is cached globally via `OnceLock` so parsing + loading
58/// happens only once per process.
59pub fn prepend_prelude_items(program: &mut Program) {
60    use shape_ast::ast::ImportItems;
61    use std::sync::OnceLock;
62
63    // Skip if program already imports from prelude (avoid double-include)
64    for item in &program.items {
65        if let Item::Import(import_stmt, _) = item {
66            if import_stmt.from == "std::core::prelude" || import_stmt.from == "std::prelude" {
67                return;
68            }
69        }
70    }
71
72    static RESOLVED_PRELUDE: OnceLock<Vec<Item>> = OnceLock::new();
73
74    let items = RESOLVED_PRELUDE.get_or_init(|| {
75        let mut loader = shape_runtime::module_loader::ModuleLoader::new();
76
77        // Load the prelude module to discover which modules it imports
78        let prelude = match loader.load_module("std::core::prelude") {
79            Ok(m) => m,
80            Err(_) => return Vec::new(),
81        };
82
83        let mut all_items = Vec::new();
84        let mut seen = std::collections::HashSet::new();
85
86        // Load each module referenced by prelude imports, selectively inlining
87        // only the items that match the import's Named spec.
88        for item in &prelude.ast.items {
89            if let Item::Import(import_stmt, _) = item {
90                let module_path = &import_stmt.from;
91                if seen.insert(module_path.clone()) {
92                    if let Ok(module) = loader.load_module(module_path) {
93                        // Build filter from Named imports
94                        let named_filter: Option<std::collections::HashSet<&str>> =
95                            match &import_stmt.items {
96                                ImportItems::Named(specs) => {
97                                    Some(specs.iter().map(|s| s.name.as_str()).collect())
98                                }
99                                ImportItems::Namespace { .. } => None,
100                            };
101
102                        if let Some(ref names) = named_filter {
103                            for ast_item in &module.ast.items {
104                                if should_include_item(ast_item, names) {
105                                    all_items.push(ast_item.clone());
106                                }
107                            }
108                        } else {
109                            all_items.extend(module.ast.items.clone());
110                        }
111                    }
112                }
113            }
114        }
115
116        all_items
117    });
118
119    if !items.is_empty() {
120        let mut prelude_items = items.clone();
121        prelude_items.extend(std::mem::take(&mut program.items));
122        program.items = prelude_items;
123    }
124}
125
126impl BytecodeExecutor {
127    /// Set a module loader for resolving file-based imports.
128    ///
129    /// When set, imports that don't match virtual modules will be resolved
130    /// by the module loader, compiled to bytecode, and merged into the program.
131    pub fn set_module_loader(&mut self, mut loader: shape_runtime::module_loader::ModuleLoader) {
132        if !self.dependency_paths.is_empty() {
133            loader.set_dependency_paths(self.dependency_paths.clone());
134        }
135        self.register_extension_artifacts_in_loader(&mut loader);
136        self.module_loader = Some(loader);
137    }
138
139    pub(crate) fn register_extension_artifacts_in_loader(
140        &self,
141        loader: &mut shape_runtime::module_loader::ModuleLoader,
142    ) {
143        for module in &self.extensions {
144            for artifact in &module.module_artifacts {
145                let code = match (&artifact.source, &artifact.compiled) {
146                    (Some(source), Some(compiled)) => ModuleCode::Both {
147                        source: std::sync::Arc::from(source.as_str()),
148                        compiled: std::sync::Arc::from(compiled.clone()),
149                    },
150                    (Some(source), None) => {
151                        ModuleCode::Source(std::sync::Arc::from(source.as_str()))
152                    }
153                    (None, Some(compiled)) => {
154                        ModuleCode::Compiled(std::sync::Arc::from(compiled.clone()))
155                    }
156                    (None, None) => continue,
157                };
158                loader.register_extension_module(artifact.module_path.clone(), code);
159            }
160
161            // Legacy fallback path mappings for extensions still using shape_sources.
162            if !module.shape_sources.is_empty() {
163                let legacy_path = format!("std::loaders::{}", module.name);
164                if !loader.has_extension_module(&legacy_path) {
165                    let source = &module.shape_sources[0].1;
166                    loader.register_extension_module(
167                        legacy_path,
168                        ModuleCode::Source(std::sync::Arc::from(source.as_str())),
169                    );
170                }
171                if !loader.has_extension_module(&module.name) {
172                    let source = &module.shape_sources[0].1;
173                    loader.register_extension_module(
174                        module.name.clone(),
175                        ModuleCode::Source(std::sync::Arc::from(source.as_str())),
176                    );
177                }
178            }
179        }
180    }
181
182    /// Get a mutable reference to the module loader (if set).
183    pub fn module_loader_mut(&mut self) -> Option<&mut shape_runtime::module_loader::ModuleLoader> {
184        self.module_loader.as_mut()
185    }
186
187    /// Pre-resolve file-based imports from a program using the module loader.
188    ///
189    /// For each import in the program that doesn't already have a virtual module,
190    /// the module loader resolves and loads the module graph. Loaded modules are
191    /// tracked so the unified compile pass can include them.
192    ///
193    /// Call this before `compile_program_impl` to enable file-based import resolution.
194    pub fn resolve_file_imports_with_context(
195        &mut self,
196        program: &Program,
197        context_dir: Option<&std::path::Path>,
198    ) {
199        use shape_ast::ast::Item;
200
201        let loader = match self.module_loader.as_mut() {
202            Some(l) => l,
203            None => return,
204        };
205        let context_dir = context_dir.map(std::path::Path::to_path_buf);
206
207        // Collect import paths that need resolution
208        let import_paths: Vec<String> = program
209            .items
210            .iter()
211            .filter_map(|item| {
212                if let Item::Import(import_stmt, _) = item {
213                    Some(import_stmt.from.clone())
214                } else {
215                    None
216                }
217            })
218            .filter(|path| !path.is_empty())
219            .collect();
220
221        for module_path in &import_paths {
222            match loader.load_module_with_context(module_path, context_dir.as_ref()) {
223                Ok(_) => {}
224                Err(e) => {
225                    // Module not found via loader — this is fine, the import might be
226                    // resolved by other means (stdlib, extensions, etc.)
227                    eprintln!(
228                        "Warning: module loader could not resolve '{}': {}",
229                        module_path, e
230                    );
231                }
232            }
233        }
234
235        // Track all loaded file modules (including transitive deps). Compilation
236        // is unified with the main program compile pipeline.
237        let mut loaded_module_paths: Vec<String> = loader
238            .loaded_modules()
239            .into_iter()
240            .map(str::to_string)
241            .collect();
242        loaded_module_paths.sort();
243
244        for module_path in loaded_module_paths {
245            self.compiled_module_paths.insert(module_path);
246        }
247    }
248
249    /// Backward-compatible wrapper without importer context.
250    pub fn resolve_file_imports(&mut self, program: &Program) {
251        self.resolve_file_imports_with_context(program, None);
252    }
253
254    /// Parse source and pre-resolve file-based imports.
255    pub fn resolve_file_imports_from_source(
256        &mut self,
257        source: &str,
258        context_dir: Option<&std::path::Path>,
259    ) {
260        match parse_program(source) {
261            Ok(program) => self.resolve_file_imports_with_context(&program, context_dir),
262            Err(e) => eprintln!(
263                "Warning: failed to parse source for import pre-resolution: {}",
264                e
265            ),
266        }
267    }
268
269    pub(crate) fn append_imported_module_items(&self, program: &mut Program) {
270        use shape_ast::ast::ImportItems;
271        let mut module_items = Vec::new();
272        let mut seen_paths = std::collections::HashSet::new();
273
274        for item in &program.items {
275            let Item::Import(import_stmt, _) = item else {
276                continue;
277            };
278            let module_path = import_stmt.from.as_str();
279            if module_path.is_empty() || !seen_paths.insert(module_path.to_string()) {
280                continue;
281            }
282
283            // Build filter from Named imports
284            let named_filter: Option<std::collections::HashSet<&str>> =
285                match &import_stmt.items {
286                    ImportItems::Named(specs) => {
287                        Some(specs.iter().map(|s| s.name.as_str()).collect())
288                    }
289                    ImportItems::Namespace { .. } => None,
290                };
291
292            let ast_items: Option<Vec<Item>> =
293                if let Some(loader) = self.module_loader.as_ref()
294                    && let Some(module) = loader.get_module(module_path)
295                {
296                    Some(module.ast.items.clone())
297                } else if let Some(source) = self.virtual_modules.get(module_path)
298                    && let Ok(parsed) = parse_program(source)
299                {
300                    Some(parsed.items)
301                } else {
302                    None
303                };
304
305            if let Some(items) = ast_items {
306                if let Some(ref names) = named_filter {
307                    for ast_item in items {
308                        if should_include_item(&ast_item, names) {
309                            module_items.push(ast_item);
310                        }
311                    }
312                } else {
313                    module_items.extend(items);
314                }
315            }
316        }
317
318        if !module_items.is_empty() {
319            module_items.extend(std::mem::take(&mut program.items));
320            program.items = module_items;
321        }
322    }
323
324    /// Create a Program from imported functions in ModuleBindingRegistry
325    pub fn create_program_from_imports(
326        module_binding_registry: &std::sync::Arc<
327            std::sync::RwLock<shape_runtime::ModuleBindingRegistry>,
328        >,
329    ) -> shape_runtime::error::Result<Program> {
330        let registry = module_binding_registry.read().unwrap();
331        let items = Vec::new();
332
333        // Extract all functions from ModuleBindingRegistry
334        for name in registry.names() {
335            if let Some(value) = registry.get_by_name(name) {
336                if value.as_closure().is_some() {
337                    // Clone the function definition - skipped for now (closures are complex)
338                    // items.push(Item::Function((*closure.function).clone(), Span::default()));
339                }
340            }
341        }
342        Ok(Program { items })
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349
350    #[test]
351    fn test_prepend_prelude_items_injects_definitions() {
352        let mut program = Program { items: vec![] };
353        prepend_prelude_items(&mut program);
354        // The prelude should inject definitions from stdlib modules
355        assert!(
356            !program.items.is_empty(),
357            "prepend_prelude_items should add items to the program"
358        );
359    }
360
361    #[test]
362    fn test_prepend_prelude_items_skips_when_already_imported() {
363        use shape_ast::ast::{ImportItems, ImportStmt, Item, Span};
364        let import = ImportStmt {
365            from: "std::core::prelude".to_string(),
366            items: ImportItems::Named(vec![]),
367        };
368        let mut program = Program {
369            items: vec![Item::Import(import, Span::DUMMY)],
370        };
371        let count_before = program.items.len();
372        prepend_prelude_items(&mut program);
373        assert_eq!(
374            program.items.len(),
375            count_before,
376            "should not inject prelude when already imported"
377        );
378    }
379
380    #[test]
381    fn test_prepend_prelude_items_idempotent() {
382        let mut program = Program { items: vec![] };
383        prepend_prelude_items(&mut program);
384        let count_after_first = program.items.len();
385        // Calling again should not add more items (user items are at end,
386        // prelude items don't contain import from std::core::prelude, but
387        // the OnceLock ensures the same items are used)
388        prepend_prelude_items(&mut program);
389        // Items will double since the skip check looks for an import statement
390        // from std::core::prelude, which we don't include. This is expected —
391        // callers should only call prepend_prelude_items once per program.
392        // The important property is that the first call works correctly.
393        assert!(count_after_first > 0);
394    }
395
396    #[test]
397    fn test_prelude_compiles_with_stdlib_definitions() {
398        // Test that compile_program_impl succeeds when prelude items are injected.
399        // The prelude injects module AST items (Display trait, Snapshot enum, math
400        // functions, etc.) directly into the program.
401        let executor = crate::configuration::BytecodeExecutor::new();
402        let mut engine =
403            shape_runtime::engine::ShapeEngine::new().expect("engine creation failed");
404        engine.load_stdlib().expect("load stdlib");
405
406        // Compile a simple program — the prelude items should be inlined.
407        let program = shape_ast::parser::parse_program("let x = 42\nx").expect("parse");
408        let bytecode = executor
409            .compile_program_for_inspection(&mut engine, &program)
410            .expect("compile with prelude should succeed");
411
412        // The prelude injects functions from std::core::math (sum, mean, etc.)
413        // and traits/enums from other modules. Verify we have more than zero
414        // functions in the compiled bytecode.
415        assert!(
416            !bytecode.functions.is_empty(),
417            "bytecode should contain prelude-injected functions"
418        );
419    }
420
421    #[test]
422    fn test_prelude_injects_math_trig_definitions() {
423        // Verify that prepend_prelude_items includes math_trig function definitions
424        let mut program = Program { items: vec![] };
425        prepend_prelude_items(&mut program);
426
427        // Check that the prelude injected some function definitions from math_trig
428        let has_fn_defs = program.items.iter().any(|item| {
429            matches!(
430                item,
431                shape_ast::ast::Item::Function(..)
432                    | shape_ast::ast::Item::Export(..)
433                    | shape_ast::ast::Item::Statement(..)
434            )
435        });
436        assert!(
437            has_fn_defs,
438            "prelude should inject function/statement definitions from stdlib modules"
439        );
440    }
441}