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::error::Result;
11use shape_ast::parser::parse_program;
12use shape_runtime::module_loader::ModuleCode;
13
14/// Check whether an AST item's name is in the given set of imported names.
15/// Items without a clear name (Impl, Extend, Import) are always included
16/// because they may be required by the named items.
17pub(crate) fn should_include_item(item: &Item, names: &std::collections::HashSet<&str>) -> bool {
18    match item {
19        Item::Function(func_def, _) => names.contains(func_def.name.as_str()),
20        Item::Export(export, _) => match &export.item {
21            ExportItem::Function(f) => names.contains(f.name.as_str()),
22            ExportItem::Enum(e) => names.contains(e.name.as_str()),
23            ExportItem::Struct(s) => names.contains(s.name.as_str()),
24            ExportItem::Trait(t) => names.contains(t.name.as_str()),
25            ExportItem::TypeAlias(a) => names.contains(a.name.as_str()),
26            ExportItem::Interface(i) => names.contains(i.name.as_str()),
27            ExportItem::ForeignFunction(f) => names.contains(f.name.as_str()),
28            ExportItem::Named(specs) => specs.iter().any(|s| names.contains(s.name.as_str())),
29        },
30        Item::StructType(def, _) => names.contains(def.name.as_str()),
31        Item::Enum(def, _) => names.contains(def.name.as_str()),
32        Item::Trait(def, _) => names.contains(def.name.as_str()),
33        Item::TypeAlias(def, _) => names.contains(def.name.as_str()),
34        Item::Interface(def, _) => names.contains(def.name.as_str()),
35        Item::VariableDecl(decl, _) => {
36            if let DestructurePattern::Identifier(name, _) = &decl.pattern {
37                names.contains(name.as_str())
38            } else {
39                false
40            }
41        }
42        // Always include impl/extend — they implement traits/methods for types
43        Item::Impl(..) | Item::Extend(..) => true,
44        // Always include sub-imports — transitive deps needed by inlined items
45        Item::Import(..) => true,
46        _ => false,
47    }
48}
49
50/// Extract function names from a list of AST items.
51pub(crate) fn collect_function_names_from_items(
52    items: &[Item],
53) -> std::collections::HashSet<String> {
54    let mut names = std::collections::HashSet::new();
55    for item in items {
56        match item {
57            Item::Function(func_def, _) => {
58                names.insert(func_def.name.clone());
59            }
60            Item::Export(export, _) => {
61                if let ExportItem::Function(f) = &export.item {
62                    names.insert(f.name.clone());
63                } else if let ExportItem::ForeignFunction(f) = &export.item {
64                    names.insert(f.name.clone());
65                }
66            }
67            _ => {}
68        }
69    }
70    names
71}
72
73/// Attach declaring package provenance to `extern C` items in a program.
74pub(crate) fn annotate_program_native_abi_package_key(
75    program: &mut Program,
76    package_key: Option<&str>,
77) {
78    let Some(package_key) = package_key else {
79        return;
80    };
81    for item in &mut program.items {
82        annotate_item_native_abi_package_key(item, package_key);
83    }
84}
85
86fn annotate_item_native_abi_package_key(item: &mut Item, package_key: &str) {
87    match item {
88        Item::ForeignFunction(def, _) => {
89            if let Some(native) = def.native_abi.as_mut()
90                && native.package_key.is_none()
91            {
92                native.package_key = Some(package_key.to_string());
93            }
94        }
95        Item::Export(export, _) => {
96            if let ExportItem::ForeignFunction(def) = &mut export.item
97                && let Some(native) = def.native_abi.as_mut()
98                && native.package_key.is_none()
99            {
100                native.package_key = Some(package_key.to_string());
101            }
102        }
103        Item::Module(module, _) => {
104            for nested in &mut module.items {
105                annotate_item_native_abi_package_key(nested, package_key);
106            }
107        }
108        _ => {}
109    }
110}
111
112/// Prepend fully-resolved prelude module AST items into the program.
113///
114/// Loads `std::core::prelude`, parses its import statements to discover which
115/// modules it references, then loads those modules and inlines their AST
116/// definitions into the program. The prelude's own import statements are NOT
117/// included (only the referenced module definitions), so `append_imported_module_items`
118/// will not double-include them.
119///
120/// The resolved prelude is cached globally via `OnceLock` so parsing + loading
121/// happens only once per process.
122///
123/// Returns the set of function names originating from `std::*` modules
124/// (used to gate `__*` internal builtin access).
125pub fn prepend_prelude_items(program: &mut Program) -> std::collections::HashSet<String> {
126    use shape_ast::ast::ImportItems;
127    use std::sync::OnceLock;
128
129    // Skip if program already imports from prelude (avoid double-include)
130    for item in &program.items {
131        if let Item::Import(import_stmt, _) = item {
132            if import_stmt.from == "std::core::prelude" || import_stmt.from == "std::prelude" {
133                return std::collections::HashSet::new();
134            }
135        }
136    }
137
138    static RESOLVED_PRELUDE: OnceLock<(Vec<Item>, std::collections::HashSet<String>)> =
139        OnceLock::new();
140
141    let (items, stdlib_names) = RESOLVED_PRELUDE.get_or_init(|| {
142        let mut loader = shape_runtime::module_loader::ModuleLoader::new();
143
144        // Load the prelude module to discover which modules it imports
145        let prelude = match loader.load_module("std::core::prelude") {
146            Ok(m) => m,
147            Err(_) => return (Vec::new(), std::collections::HashSet::new()),
148        };
149
150        let mut all_items = Vec::new();
151        let mut seen = std::collections::HashSet::new();
152
153        // Load each module referenced by prelude imports, selectively inlining
154        // only the items that match the import's Named spec.
155        for item in &prelude.ast.items {
156            if let Item::Import(import_stmt, _) = item {
157                let module_path = &import_stmt.from;
158                if seen.insert(module_path.clone()) {
159                    if let Ok(module) = loader.load_module(module_path) {
160                        // Build filter from Named imports
161                        let named_filter: Option<std::collections::HashSet<&str>> =
162                            match &import_stmt.items {
163                                ImportItems::Named(specs) => {
164                                    Some(specs.iter().map(|s| s.name.as_str()).collect())
165                                }
166                                ImportItems::Namespace { .. } => None,
167                            };
168
169                        if let Some(ref names) = named_filter {
170                            for ast_item in &module.ast.items {
171                                if should_include_item(ast_item, names) {
172                                    all_items.push(ast_item.clone());
173                                }
174                            }
175                        } else {
176                            all_items.extend(module.ast.items.clone());
177                        }
178                    }
179                }
180            }
181        }
182
183        let stdlib_names = collect_function_names_from_items(&all_items);
184        (all_items, stdlib_names)
185    });
186
187    if !items.is_empty() {
188        let mut prelude_items = items.clone();
189        prelude_items.extend(std::mem::take(&mut program.items));
190        program.items = prelude_items;
191    }
192
193    stdlib_names.clone()
194}
195
196impl BytecodeExecutor {
197    /// Set a module loader for resolving file-based imports.
198    ///
199    /// When set, imports that don't match virtual modules will be resolved
200    /// by the module loader, compiled to bytecode, and merged into the program.
201    pub fn set_module_loader(&mut self, mut loader: shape_runtime::module_loader::ModuleLoader) {
202        if !self.dependency_paths.is_empty() {
203            loader.set_dependency_paths(self.dependency_paths.clone());
204        }
205        self.register_extension_artifacts_in_loader(&mut loader);
206        self.module_loader = Some(loader);
207    }
208
209    pub(crate) fn register_extension_artifacts_in_loader(
210        &self,
211        loader: &mut shape_runtime::module_loader::ModuleLoader,
212    ) {
213        for module in &self.extensions {
214            for artifact in &module.module_artifacts {
215                let code = match (&artifact.source, &artifact.compiled) {
216                    (Some(source), Some(compiled)) => ModuleCode::Both {
217                        source: std::sync::Arc::from(source.as_str()),
218                        compiled: std::sync::Arc::from(compiled.clone()),
219                    },
220                    (Some(source), None) => {
221                        ModuleCode::Source(std::sync::Arc::from(source.as_str()))
222                    }
223                    (None, Some(compiled)) => {
224                        ModuleCode::Compiled(std::sync::Arc::from(compiled.clone()))
225                    }
226                    (None, None) => continue,
227                };
228                loader.register_extension_module(artifact.module_path.clone(), code);
229            }
230
231            // Legacy fallback path mappings for extensions still using shape_sources.
232            if !module.shape_sources.is_empty() {
233                let legacy_path = format!("std::loaders::{}", module.name);
234                if !loader.has_extension_module(&legacy_path) {
235                    let source = &module.shape_sources[0].1;
236                    loader.register_extension_module(
237                        legacy_path,
238                        ModuleCode::Source(std::sync::Arc::from(source.as_str())),
239                    );
240                }
241                if !loader.has_extension_module(&module.name) {
242                    let source = &module.shape_sources[0].1;
243                    loader.register_extension_module(
244                        module.name.clone(),
245                        ModuleCode::Source(std::sync::Arc::from(source.as_str())),
246                    );
247                }
248            }
249        }
250    }
251
252    /// Get a mutable reference to the module loader (if set).
253    pub fn module_loader_mut(&mut self) -> Option<&mut shape_runtime::module_loader::ModuleLoader> {
254        self.module_loader.as_mut()
255    }
256
257    /// Pre-resolve file-based imports from a program using the module loader.
258    ///
259    /// For each import in the program that doesn't already have a virtual module,
260    /// the module loader resolves and loads the module graph. Loaded modules are
261    /// tracked so the unified compile pass can include them.
262    ///
263    /// Call this before `compile_program_impl` to enable file-based import resolution.
264    pub fn resolve_file_imports_with_context(
265        &mut self,
266        program: &Program,
267        context_dir: Option<&std::path::Path>,
268    ) {
269        use shape_ast::ast::Item;
270
271        let loader = match self.module_loader.as_mut() {
272            Some(l) => l,
273            None => return,
274        };
275        let context_dir = context_dir.map(std::path::Path::to_path_buf);
276
277        // Collect import paths that need resolution
278        let import_paths: Vec<String> = program
279            .items
280            .iter()
281            .filter_map(|item| {
282                if let Item::Import(import_stmt, _) = item {
283                    Some(import_stmt.from.clone())
284                } else {
285                    None
286                }
287            })
288            .filter(|path| !path.is_empty())
289            .collect();
290
291        for module_path in &import_paths {
292            match loader.load_module_with_context(module_path, context_dir.as_ref()) {
293                Ok(_) => {}
294                Err(e) => {
295                    // Module not found via loader — this is fine, the import might be
296                    // resolved by other means (stdlib, extensions, etc.)
297                    eprintln!(
298                        "Warning: module loader could not resolve '{}': {}",
299                        module_path, e
300                    );
301                }
302            }
303        }
304
305        // Track all loaded file modules (including transitive deps). Compilation
306        // is unified with the main program compile pipeline.
307        let mut loaded_module_paths: Vec<String> = loader
308            .loaded_modules()
309            .into_iter()
310            .map(str::to_string)
311            .collect();
312        loaded_module_paths.sort();
313
314        for module_path in loaded_module_paths {
315            self.compiled_module_paths.insert(module_path);
316        }
317    }
318
319    /// Backward-compatible wrapper without importer context.
320    pub fn resolve_file_imports(&mut self, program: &Program) {
321        self.resolve_file_imports_with_context(program, None);
322    }
323
324    /// Parse source and pre-resolve file-based imports.
325    pub fn resolve_file_imports_from_source(
326        &mut self,
327        source: &str,
328        context_dir: Option<&std::path::Path>,
329    ) {
330        match parse_program(source) {
331            Ok(program) => self.resolve_file_imports_with_context(&program, context_dir),
332            Err(e) => eprintln!(
333                "Warning: failed to parse source for import pre-resolution: {}",
334                e
335            ),
336        }
337    }
338
339    /// Inline AST items from imported modules into the program.
340    ///
341    /// Uses an iterative fixed-point loop to resolve transitive imports
342    /// (imports within inlined module items).
343    ///
344    /// Returns the set of function names originating from `std::*` modules.
345    pub(crate) fn append_imported_module_items(
346        &mut self,
347        program: &mut Program,
348    ) -> Result<std::collections::HashSet<String>> {
349        use shape_ast::ast::ImportItems;
350        // Track which specific names have been inlined from each module path.
351        // For namespace (wildcard) imports, the path is stored with None (= all items).
352        let mut inlined_names: std::collections::HashMap<
353            String,
354            Option<std::collections::HashSet<String>>,
355        > = std::collections::HashMap::new();
356        let mut stdlib_names = std::collections::HashSet::new();
357
358        loop {
359            let mut module_items = Vec::new();
360            let mut found_new = false;
361
362            // Collect import statements, merging named filters per module path.
363            // A module path that was previously inlined with a wildcard import
364            // needs no further processing. Named imports only need to resolve
365            // names not yet inlined.
366            let mut merged: std::collections::HashMap<
367                String,
368                Option<std::collections::HashSet<String>>,
369            > = std::collections::HashMap::new();
370
371            for item in program.items.iter() {
372                let Item::Import(import_stmt, _) = item else {
373                    continue;
374                };
375                let module_path = import_stmt.from.as_str();
376                if module_path.is_empty() {
377                    continue;
378                }
379
380                // If this path was already fully inlined (wildcard), skip
381                if matches!(inlined_names.get(module_path), Some(None)) {
382                    continue;
383                }
384
385                let named_filter: Option<std::collections::HashSet<String>> =
386                    match &import_stmt.items {
387                        ImportItems::Named(specs) => {
388                            Some(specs.iter().map(|s| s.name.clone()).collect())
389                        }
390                        ImportItems::Namespace { .. } => None,
391                    };
392
393                // Filter out already-inlined names
394                let new_filter = match &named_filter {
395                    None => {
396                        // Wildcard import — only new if not previously wildcarded
397                        if matches!(inlined_names.get(module_path), Some(None)) {
398                            continue;
399                        }
400                        None
401                    }
402                    Some(names) => {
403                        let mut new_names = names.clone();
404                        if let Some(Some(already)) = inlined_names.get(module_path) {
405                            new_names.retain(|n| !already.contains(n));
406                        }
407                        if new_names.is_empty() {
408                            continue;
409                        }
410                        Some(new_names)
411                    }
412                };
413
414                // Merge into this iteration's work
415                let entry = merged
416                    .entry(module_path.to_string())
417                    .or_insert_with(|| Some(std::collections::HashSet::new()));
418                match new_filter {
419                    None => {
420                        // Upgrade to wildcard
421                        *entry = None;
422                    }
423                    Some(ref new) => {
424                        if let Some(existing) = entry {
425                            existing.extend(new.iter().cloned());
426                        }
427                        // If entry is None (wildcard), keep it
428                    }
429                }
430            }
431
432            for (module_path, merged_filter) in &merged {
433                found_new = true;
434                let is_std = module_path.starts_with("std::");
435
436                // Try loading the module
437                let ast_items: Option<Vec<Item>> = if let Some(loader) = self.module_loader.as_mut()
438                {
439                    if let Some(module) = loader.get_module(module_path) {
440                        Some(module.ast.items.clone())
441                    } else {
442                        Some(loader.load_module(module_path)?.ast.items.clone())
443                    }
444                } else {
445                    None
446                };
447
448                let ast_items = match ast_items {
449                    Some(items) => Some(items),
450                    None => match self.virtual_modules.get(module_path.as_str()) {
451                        Some(source) => Some(parse_program(source)?.items),
452                        None => None,
453                    },
454                };
455
456                if let Some(items) = ast_items {
457                    if is_std {
458                        stdlib_names.extend(collect_function_names_from_items(&items));
459                    }
460                    if let Some(names) = merged_filter {
461                        let names_ref: std::collections::HashSet<&str> =
462                            names.iter().map(|s| s.as_str()).collect();
463                        for ast_item in items {
464                            if should_include_item(&ast_item, &names_ref) {
465                                module_items.push(ast_item);
466                            }
467                        }
468                        // Record inlined names
469                        let entry = inlined_names
470                            .entry(module_path.clone())
471                            .or_insert_with(|| Some(std::collections::HashSet::new()));
472                        if let Some(existing) = entry {
473                            existing.extend(names.iter().cloned());
474                        }
475                    } else {
476                        module_items.extend(items);
477                        // Record as fully inlined
478                        inlined_names.insert(module_path.clone(), None);
479                    }
480                }
481            }
482
483            if !module_items.is_empty() {
484                module_items.extend(std::mem::take(&mut program.items));
485                program.items = module_items;
486            }
487
488            if !found_new {
489                break;
490            }
491        }
492
493        Ok(stdlib_names)
494    }
495
496    /// Create a Program from imported functions in ModuleBindingRegistry
497    pub fn create_program_from_imports(
498        module_binding_registry: &std::sync::Arc<
499            std::sync::RwLock<shape_runtime::ModuleBindingRegistry>,
500        >,
501    ) -> shape_runtime::error::Result<Program> {
502        let registry = module_binding_registry.read().unwrap();
503        let items = Vec::new();
504
505        // Extract all functions from ModuleBindingRegistry
506        for name in registry.names() {
507            if let Some(value) = registry.get_by_name(name) {
508                if value.as_closure().is_some() {
509                    // Clone the function definition - skipped for now (closures are complex)
510                    // items.push(Item::Function((*closure.function).clone(), Span::default()));
511                }
512            }
513        }
514        Ok(Program {
515            items,
516            docs: shape_ast::ast::ProgramDocs::default(),
517        })
518    }
519}
520
521#[cfg(test)]
522mod tests {
523    use super::*;
524
525    #[test]
526    fn test_prepend_prelude_items_injects_definitions() {
527        let mut program = Program {
528            items: vec![],
529            docs: shape_ast::ast::ProgramDocs::default(),
530        };
531        prepend_prelude_items(&mut program);
532        // The prelude should inject definitions from stdlib modules
533        assert!(
534            !program.items.is_empty(),
535            "prepend_prelude_items should add items to the program"
536        );
537    }
538
539    #[test]
540    fn test_prepend_prelude_items_skips_when_already_imported() {
541        use shape_ast::ast::{ImportItems, ImportStmt, Item, Span};
542        let import = ImportStmt {
543            from: "std::core::prelude".to_string(),
544            items: ImportItems::Named(vec![]),
545        };
546        let mut program = Program {
547            items: vec![Item::Import(import, Span::DUMMY)],
548            docs: shape_ast::ast::ProgramDocs::default(),
549        };
550        let count_before = program.items.len();
551        prepend_prelude_items(&mut program);
552        assert_eq!(
553            program.items.len(),
554            count_before,
555            "should not inject prelude when already imported"
556        );
557    }
558
559    #[test]
560    fn test_prepend_prelude_items_idempotent() {
561        let mut program = Program {
562            items: vec![],
563            docs: shape_ast::ast::ProgramDocs::default(),
564        };
565        prepend_prelude_items(&mut program);
566        let count_after_first = program.items.len();
567        // Calling again should not add more items (user items are at end,
568        // prelude items don't contain import from std::core::prelude, but
569        // the OnceLock ensures the same items are used)
570        prepend_prelude_items(&mut program);
571        // Items will double since the skip check looks for an import statement
572        // from std::core::prelude, which we don't include. This is expected —
573        // callers should only call prepend_prelude_items once per program.
574        // The important property is that the first call works correctly.
575        assert!(count_after_first > 0);
576    }
577
578    #[test]
579    fn test_prelude_compiles_with_stdlib_definitions() {
580        // Test that compile_program_impl succeeds when prelude items are injected.
581        // The prelude injects module AST items (Display trait, Snapshot enum, math
582        // functions, etc.) directly into the program.
583        let mut executor = crate::configuration::BytecodeExecutor::new();
584        let mut engine = shape_runtime::engine::ShapeEngine::new().expect("engine creation failed");
585        engine.load_stdlib().expect("load stdlib");
586
587        // Compile a simple program — the prelude items should be inlined.
588        let program = shape_ast::parser::parse_program("let x = 42\nx").expect("parse");
589        let bytecode = executor
590            .compile_program_for_inspection(&mut engine, &program)
591            .expect("compile with prelude should succeed");
592
593        // The prelude injects functions from std::core::math (sum, mean, etc.)
594        // and traits/enums from other modules. Verify we have more than zero
595        // functions in the compiled bytecode.
596        assert!(
597            !bytecode.functions.is_empty(),
598            "bytecode should contain prelude-injected functions"
599        );
600    }
601
602    #[test]
603    fn test_prelude_injects_math_trig_definitions() {
604        // Verify that prepend_prelude_items includes math_trig function definitions
605        let mut program = Program {
606            items: vec![],
607            docs: shape_ast::ast::ProgramDocs::default(),
608        };
609        prepend_prelude_items(&mut program);
610
611        // Check that the prelude injected some function definitions from math_trig
612        let has_fn_defs = program.items.iter().any(|item| {
613            matches!(
614                item,
615                shape_ast::ast::Item::Function(..)
616                    | shape_ast::ast::Item::Export(..)
617                    | shape_ast::ast::Item::Statement(..)
618            )
619        });
620        assert!(
621            has_fn_defs,
622            "prelude should inject function/statement definitions from stdlib modules"
623        );
624    }
625}