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