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::{ExportItem, Item};
10use shape_ast::parser::parse_program;
11use shape_runtime::module_loader::ModuleCode;
12
13pub(crate) fn hidden_annotation_import_module_name(module_path: &str) -> String {
14    use std::hash::{Hash, Hasher};
15
16    let mut hasher = std::collections::hash_map::DefaultHasher::new();
17    module_path.hash(&mut hasher);
18    format!("__annimport__{:016x}", hasher.finish())
19}
20
21pub(crate) fn is_hidden_annotation_import_module_name(name: &str) -> bool {
22    name.starts_with("__annimport__")
23}
24
25/// Build a module graph and compute stdlib names from the prelude modules.
26///
27/// This is the canonical entry point for graph-based compilation. It:
28/// 1. Collects prelude import paths from the module loader
29/// 2. Builds the full module dependency graph
30/// 3. Computes stdlib function names from prelude module interfaces
31///
32/// Returns `(graph, stdlib_names, prelude_imports)`.
33pub fn build_graph_and_stdlib_names(
34    program: &Program,
35    loader: &mut shape_runtime::module_loader::ModuleLoader,
36    extensions: &[shape_runtime::module_exports::ModuleExports],
37) -> std::result::Result<
38    (
39        std::sync::Arc<crate::module_graph::ModuleGraph>,
40        std::collections::HashSet<String>,
41        Vec<String>,
42    ),
43    shape_ast::error::ShapeError,
44> {
45    let prelude_imports = crate::module_graph::collect_prelude_import_paths(loader);
46    let graph =
47        crate::module_graph::build_module_graph(program, loader, extensions, &prelude_imports)
48            .map_err(|e| shape_ast::error::ShapeError::ModuleError {
49                message: e.to_string(),
50                module_path: None,
51            })?;
52    let graph = std::sync::Arc::new(graph);
53
54    let mut stdlib_names = std::collections::HashSet::new();
55    for prelude_path in &prelude_imports {
56        if let Some(dep_id) = graph.id_for_path(prelude_path) {
57            let dep_node = graph.node(dep_id);
58            for export_name in dep_node.interface.exports.keys() {
59                stdlib_names.insert(export_name.clone());
60                stdlib_names.insert(format!("{}::{}", prelude_path, export_name));
61            }
62        }
63    }
64
65    Ok((graph, stdlib_names, prelude_imports))
66}
67
68/// Attach declaring package provenance to `extern C` items in a program.
69pub(crate) fn annotate_program_native_abi_package_key(
70    program: &mut Program,
71    package_key: Option<&str>,
72) {
73    let Some(package_key) = package_key else {
74        return;
75    };
76    for item in &mut program.items {
77        annotate_item_native_abi_package_key(item, package_key);
78    }
79}
80
81fn annotate_item_native_abi_package_key(item: &mut Item, package_key: &str) {
82    match item {
83        Item::ForeignFunction(def, _) => {
84            if let Some(native) = def.native_abi.as_mut()
85                && native.package_key.is_none()
86            {
87                native.package_key = Some(package_key.to_string());
88            }
89        }
90        Item::Export(export, _) => {
91            if let ExportItem::ForeignFunction(def) = &mut export.item
92                && let Some(native) = def.native_abi.as_mut()
93                && native.package_key.is_none()
94            {
95                native.package_key = Some(package_key.to_string());
96            }
97        }
98        Item::Module(module, _) => {
99            for nested in &mut module.items {
100                annotate_item_native_abi_package_key(nested, package_key);
101            }
102        }
103        _ => {}
104    }
105}
106
107
108impl BytecodeExecutor {
109    /// Set a module loader for resolving file-based imports.
110    ///
111    /// When set, imports that don't match virtual modules will be resolved
112    /// by the module loader, compiled to bytecode, and merged into the program.
113    pub fn set_module_loader(&mut self, mut loader: shape_runtime::module_loader::ModuleLoader) {
114        if !self.dependency_paths.is_empty() {
115            loader.set_dependency_paths(self.dependency_paths.clone());
116        }
117        self.register_extension_artifacts_in_loader(&mut loader);
118        self.module_loader = Some(loader);
119    }
120
121    pub(crate) fn register_extension_artifacts_in_loader(
122        &self,
123        loader: &mut shape_runtime::module_loader::ModuleLoader,
124    ) {
125        for module in &self.extensions {
126            for artifact in &module.module_artifacts {
127                let code = match (&artifact.source, &artifact.compiled) {
128                    (Some(source), Some(compiled)) => ModuleCode::Both {
129                        source: std::sync::Arc::from(source.as_str()),
130                        compiled: std::sync::Arc::from(compiled.clone()),
131                    },
132                    (Some(source), None) => {
133                        ModuleCode::Source(std::sync::Arc::from(source.as_str()))
134                    }
135                    (None, Some(compiled)) => {
136                        ModuleCode::Compiled(std::sync::Arc::from(compiled.clone()))
137                    }
138                    (None, None) => continue,
139                };
140                loader.register_extension_module(artifact.module_path.clone(), code);
141            }
142
143            // Register shape_sources under the module's canonical name only.
144            for (_filename, source) in &module.shape_sources {
145                if !loader.has_extension_module(&module.name) {
146                    loader.register_extension_module(
147                        module.name.clone(),
148                        ModuleCode::Source(std::sync::Arc::from(source.as_str())),
149                    );
150                }
151            }
152        }
153    }
154
155    /// Get a mutable reference to the module loader (if set).
156    pub fn module_loader_mut(&mut self) -> Option<&mut shape_runtime::module_loader::ModuleLoader> {
157        self.module_loader.as_mut()
158    }
159
160    /// Pre-resolve file-based imports from a program using the module loader.
161    ///
162    /// For each import in the program that doesn't already have a virtual module,
163    /// the module loader resolves and loads the module graph. Loaded modules are
164    /// tracked so the unified compile pass can include them.
165    ///
166    /// Call this before `compile_program_impl` to enable file-based import resolution.
167    pub fn resolve_file_imports_with_context(
168        &mut self,
169        program: &Program,
170        context_dir: Option<&std::path::Path>,
171    ) {
172        use shape_ast::ast::Item;
173
174        let loader = match self.module_loader.as_mut() {
175            Some(l) => l,
176            None => return,
177        };
178        let context_dir = context_dir.map(std::path::Path::to_path_buf);
179
180        // Collect import paths that need resolution
181        let import_paths: Vec<String> = program
182            .items
183            .iter()
184            .filter_map(|item| {
185                if let Item::Import(import_stmt, _) = item {
186                    Some(import_stmt.from.clone())
187                } else {
188                    None
189                }
190            })
191            .filter(|path| !path.is_empty())
192            .collect();
193
194        for module_path in &import_paths {
195            // Pre-resolution: attempt to load each import path. Failures are
196            // silently ignored here because the module may be resolved later
197            // via virtual modules, embedded stdlib, or extension resolvers.
198            let _ = loader.load_module_with_context(module_path, context_dir.as_ref());
199        }
200
201        // Track all loaded file modules (including transitive deps). Compilation
202        // is unified with the main program compile pipeline.
203        let mut loaded_module_paths: Vec<String> = loader
204            .loaded_modules()
205            .into_iter()
206            .map(str::to_string)
207            .collect();
208        loaded_module_paths.sort();
209
210        for module_path in loaded_module_paths {
211            self.compiled_module_paths.insert(module_path);
212        }
213    }
214
215    /// Backward-compatible wrapper without importer context.
216    pub fn resolve_file_imports(&mut self, program: &Program) {
217        self.resolve_file_imports_with_context(program, None);
218    }
219
220    /// Parse source and pre-resolve file-based imports.
221    pub fn resolve_file_imports_from_source(
222        &mut self,
223        source: &str,
224        context_dir: Option<&std::path::Path>,
225    ) {
226        match parse_program(source) {
227            Ok(program) => self.resolve_file_imports_with_context(&program, context_dir),
228            Err(e) => eprintln!(
229                "Warning: failed to parse source for import pre-resolution: {}",
230                e
231            ),
232        }
233    }
234
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use crate::VMConfig;
241    use crate::compiler::BytecodeCompiler;
242    use crate::executor::VirtualMachine;
243    use crate::module_graph;
244
245    /// Helper: build a graph and compile a program with prelude + imports.
246    fn compile_program_with_graph(
247        source: &str,
248        extra_paths: &[std::path::PathBuf],
249    ) -> shape_ast::error::Result<crate::bytecode::BytecodeProgram> {
250        let program = shape_ast::parser::parse_program(source)?;
251        let mut loader = shape_runtime::module_loader::ModuleLoader::new();
252        for p in extra_paths {
253            loader.add_module_path(p.clone());
254        }
255        let prelude_imports = module_graph::collect_prelude_import_paths(&mut loader);
256        let graph = module_graph::build_module_graph(&program, &mut loader, &[], &prelude_imports)
257            .map_err(|e| shape_ast::error::ShapeError::ModuleError {
258                message: e.to_string(),
259                module_path: None,
260            })?;
261        let graph = std::sync::Arc::new(graph);
262
263        let mut stdlib_names = std::collections::HashSet::new();
264        for prelude_path in &prelude_imports {
265            if let Some(dep_id) = graph.id_for_path(prelude_path) {
266                let dep_node = graph.node(dep_id);
267                for export_name in dep_node.interface.exports.keys() {
268                    stdlib_names.insert(export_name.clone());
269                    stdlib_names.insert(format!("{}::{}", prelude_path, export_name));
270                }
271            }
272        }
273
274        let mut compiler = BytecodeCompiler::new();
275        compiler.stdlib_function_names = stdlib_names;
276        compiler.compile_with_graph_and_prelude(&program, graph, &prelude_imports)
277    }
278
279    #[test]
280    fn test_graph_prelude_provides_stdlib_definitions() {
281        // Verify the graph pipeline compiles a simple program with prelude.
282        let bytecode = compile_program_with_graph("let x = 42\nx", &[])
283            .expect("compile with graph prelude should succeed");
284        assert!(
285            !bytecode.functions.is_empty(),
286            "bytecode should contain prelude-compiled functions"
287        );
288    }
289
290    #[test]
291    fn test_graph_prelude_includes_math_functions() {
292        // Verify prelude modules appear in the graph and provide exports.
293        let program = shape_ast::parser::parse_program("let x = 1\nx").expect("parse");
294        let mut loader = shape_runtime::module_loader::ModuleLoader::new();
295        let prelude_imports = module_graph::collect_prelude_import_paths(&mut loader);
296        let graph =
297            module_graph::build_module_graph(&program, &mut loader, &[], &prelude_imports)
298                .expect("graph build");
299
300        // The prelude should load std::core::math
301        let math_id = graph.id_for_path("std::core::math");
302        assert!(math_id.is_some(), "graph should contain std::core::math");
303
304        let math_node = graph.node(math_id.unwrap());
305        assert!(
306            math_node.interface.exports.contains_key("sum"),
307            "std::core::math should export 'sum'"
308        );
309    }
310
311    #[test]
312    fn test_graph_compiles_with_engine() {
313        // Test that compile_program_for_inspection succeeds via graph pipeline.
314        let mut executor = crate::configuration::BytecodeExecutor::new();
315        let mut engine =
316            shape_runtime::engine::ShapeEngine::new().expect("engine creation failed");
317        engine.load_stdlib().expect("load stdlib");
318
319        let program = shape_ast::parser::parse_program("let x = 42\nx").expect("parse");
320        let bytecode = executor
321            .compile_program_for_inspection(&mut engine, &program)
322            .expect("compile with graph pipeline should succeed");
323
324        assert!(
325            !bytecode.functions.is_empty(),
326            "bytecode should contain prelude-compiled functions"
327        );
328    }
329
330    #[test]
331    fn test_graph_file_dependency_named_import() {
332        // Test that named imports from file dependencies work with the graph.
333        let tmp = tempfile::tempdir().expect("temp dir");
334        let mod_dir = tmp.path().join("mymod");
335        std::fs::create_dir_all(&mod_dir).expect("create mymod dir");
336        std::fs::write(
337            mod_dir.join("index.shape"),
338            r#"
339pub fn alpha() -> int { 1 }
340pub fn beta() -> int { 2 }
341pub fn gamma() -> int { 3 }
342"#,
343        )
344        .expect("write index.shape");
345
346        let source = r#"
347from mymod use { alpha, beta, gamma }
348alpha() + beta() + gamma()
349"#;
350        let bytecode = compile_program_with_graph(source, &[tmp.path().to_path_buf()])
351            .expect("named import from file dependency should compile");
352
353        let mut vm = VirtualMachine::new(VMConfig::default());
354        vm.load_program(bytecode);
355        let result = vm.execute(None).expect("execute");
356        assert_eq!(result.as_number_coerce().unwrap(), 6.0);
357    }
358
359    #[test]
360    fn test_graph_namespace_import_enables_qualified_calls() {
361        let tmp = tempfile::tempdir().expect("temp dir");
362        let mod_dir = tmp.path().join("mymod");
363        std::fs::create_dir_all(&mod_dir).expect("create module dir");
364        std::fs::write(
365            mod_dir.join("index.shape"),
366            r#"
367pub fn alpha() -> int { 1 }
368pub fn beta() -> int { alpha() + 1 }
369"#,
370        )
371        .expect("write index.shape");
372
373        let bytecode = compile_program_with_graph(
374            r#"
375use mymod
376mymod::beta()
377"#,
378            &[tmp.path().to_path_buf()],
379        )
380        .expect("namespace call should compile");
381
382        let mut vm = VirtualMachine::new(VMConfig::default());
383        vm.load_program(bytecode);
384        let result = vm.execute(None).expect("execute");
385        assert_eq!(result.as_number_coerce().unwrap(), 2.0);
386    }
387
388    #[test]
389    fn test_graph_cycle_detection() {
390        // Verify that circular imports are rejected with a clear error.
391        let tmp = tempfile::tempdir().expect("temp dir");
392        std::fs::write(
393            tmp.path().join("a.shape"),
394            "use b\npub fn fa() -> int { 1 }\n",
395        )
396        .expect("write a.shape");
397        std::fs::write(
398            tmp.path().join("b.shape"),
399            "use a\npub fn fb() -> int { 2 }\n",
400        )
401        .expect("write b.shape");
402
403        let source = "use a\na::fa()\n";
404        let result = compile_program_with_graph(source, &[tmp.path().to_path_buf()]);
405        assert!(
406            result.is_err(),
407            "circular import should produce an error"
408        );
409        let err_msg = format!("{}", result.unwrap_err());
410        assert!(
411            err_msg.to_lowercase().contains("circular")
412                || err_msg.to_lowercase().contains("cyclic"),
413            "error should mention circularity, got: {}",
414            err_msg
415        );
416    }
417
418    #[test]
419    fn test_graph_stdlib_names_include_qualified() {
420        // Verify that stdlib names include both bare and qualified names.
421        let program = shape_ast::parser::parse_program("1").expect("parse");
422        let mut loader = shape_runtime::module_loader::ModuleLoader::new();
423        let prelude_imports = module_graph::collect_prelude_import_paths(&mut loader);
424        let graph =
425            module_graph::build_module_graph(&program, &mut loader, &[], &prelude_imports)
426                .expect("graph build");
427
428        let mut stdlib_names = std::collections::HashSet::new();
429        for prelude_path in &prelude_imports {
430            if let Some(dep_id) = graph.id_for_path(prelude_path) {
431                let dep_node = graph.node(dep_id);
432                for export_name in dep_node.interface.exports.keys() {
433                    stdlib_names.insert(export_name.clone());
434                    stdlib_names.insert(format!("{}::{}", prelude_path, export_name));
435                }
436            }
437        }
438
439        assert!(
440            stdlib_names.contains("sum"),
441            "stdlib_names should contain bare name 'sum'"
442        );
443        assert!(
444            stdlib_names.contains("std::core::math::sum"),
445            "stdlib_names should contain qualified name 'std::core::math::sum'"
446        );
447    }
448
449    /// Regression: function body references a type alias defined later in the
450    /// same program.  Under graph compilation the first-pass must register the
451    /// alias in both `type_aliases` and `type_inference.env` so that
452    /// `resolve_type_name` and `lookup_type_alias` find it when compiling the
453    /// function body.
454    #[test]
455    fn test_type_alias_forward_reference_under_graph_compilation() {
456        // The alias is defined AFTER the function that uses it —
457        // this is a true forward reference.
458        let bytecode = compile_program_with_graph(
459            r#"
460            fn make_val() -> MyInt { 42 }
461            type MyInt = int
462            make_val()
463            "#,
464            &[],
465        )
466        .expect("compile with forward type alias should succeed");
467        let mut vm = VirtualMachine::new(VMConfig::default());
468        vm.load_program(bytecode);
469        let result = vm.execute(None).expect("execute failed");
470        assert_eq!(result.as_i64(), Some(42));
471    }
472}