Skip to main content

shape_runtime/module_loader/
mod.rs

1//! Module loading and management for Shape
2//!
3//! This module handles loading, compiling, and caching Shape modules
4//! from both the standard library and user-defined sources.
5
6mod cache;
7mod loading;
8mod resolution;
9#[cfg(all(test, feature = "deep-tests"))]
10mod resolution_deep_tests;
11mod resolver;
12
13use crate::project::{DependencySpec, ProjectRoot, find_project_root, normalize_package_identity};
14use shape_ast::ast::{FunctionDef, ImportStmt, Program, Span};
15use shape_ast::error::{Result, ShapeError};
16use shape_ast::parser::parse_program;
17use shape_value::ValueWord;
18use std::collections::HashMap;
19use std::path::{Path, PathBuf};
20use std::sync::Arc;
21
22use cache::ModuleCache;
23pub use resolver::{
24    FilesystemResolver, InMemoryResolver, ModuleCode, ModuleResolver, ResolvedModuleArtifact,
25};
26
27include!(concat!(env!("OUT_DIR"), "/embedded_stdlib_modules.rs"));
28
29/// A compiled module ready for execution
30#[derive(Debug, Clone)]
31pub struct Module {
32    pub name: String,
33    pub path: String,
34    pub exports: HashMap<String, Export>,
35    pub ast: Program,
36}
37
38impl Module {
39    /// Get an exported item by name
40    pub fn get_export(&self, name: &str) -> Option<&Export> {
41        self.exports.get(name)
42    }
43
44    /// Get all export names
45    pub fn export_names(&self) -> Vec<&str> {
46        self.exports.keys().map(|s| s.as_str()).collect()
47    }
48}
49
50/// An exported item from a module
51#[derive(Debug, Clone)]
52pub enum Export {
53    Function(Arc<FunctionDef>),
54    TypeAlias(Arc<shape_ast::ast::TypeAliasDef>),
55    Value(ValueWord),
56}
57
58/// Kind of exported symbol discovered from module source.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum ModuleExportKind {
61    Function,
62    TypeAlias,
63    Interface,
64    Enum,
65    Value,
66}
67
68/// Exported symbol metadata used by tooling (LSP, analyzers).
69#[derive(Debug, Clone)]
70pub struct ModuleExportSymbol {
71    /// Original symbol name in module scope.
72    pub name: String,
73    /// Alias if exported as `name as alias`.
74    pub alias: Option<String>,
75    /// High-level symbol kind.
76    pub kind: ModuleExportKind,
77    /// Source span for navigation/diagnostics.
78    pub span: Span,
79}
80
81/// Collect exported symbols from a parsed module AST using runtime export semantics.
82pub fn collect_exported_symbols(program: &Program) -> Result<Vec<ModuleExportSymbol>> {
83    loading::collect_exported_symbols(program)
84}
85
86/// Collect exported function names from module source using canonical
87/// module-loader export semantics.
88///
89/// This keeps extension module namespace behavior (`use mod; mod.fn(...)`)
90/// aligned with normal module loading and avoids ad-hoc export parsing.
91pub fn collect_exported_function_names_from_source(
92    module_path: &str,
93    source: &str,
94) -> Result<Vec<String>> {
95    let ast = parse_program(source).map_err(|e| ShapeError::ModuleError {
96        message: format!("Failed to parse module source '{}': {}", module_path, e),
97        module_path: None,
98    })?;
99
100    let module = loading::compile_module(module_path, ast)?;
101    let mut names: Vec<String> = module
102        .exports
103        .into_iter()
104        .filter_map(|(name, export)| match export {
105            Export::Function(_) => Some(name),
106            _ => None,
107        })
108        .collect();
109    names.sort();
110    names.dedup();
111    Ok(names)
112}
113
114/// Module loader manages loading and caching of modules
115pub struct ModuleLoader {
116    /// Standard library modules (built-in)
117    stdlib_path: PathBuf,
118    /// User module search paths
119    module_paths: Vec<PathBuf>,
120    /// Active project root used to attribute loaded filesystem modules.
121    current_project_root: Option<PathBuf>,
122    /// Module cache and dependency tracking
123    cache: ModuleCache,
124    /// Resolved dependency paths (name -> local path).
125    /// Populated by the dependency resolver after resolving shape.toml deps.
126    dependency_paths: HashMap<String, PathBuf>,
127    /// Extension-provided in-memory modules (highest priority).
128    extension_resolver: InMemoryResolver,
129    /// Bundle-provided in-memory modules (between extension and embedded stdlib).
130    bundle_resolver: InMemoryResolver,
131    /// Embedded stdlib in-memory modules (before filesystem fallback).
132    embedded_stdlib_resolver: InMemoryResolver,
133    /// Optional keychain for verifying module signatures.
134    keychain: Option<crate::crypto::Keychain>,
135    /// Optional external blob store for lazy-fetching content-addressed blobs
136    /// that are not found in the inline blob cache.
137    blob_store: Option<Arc<dyn crate::blob_store::BlobStore>>,
138}
139
140impl ModuleLoader {
141    /// Create a new module loader
142    pub fn new() -> Self {
143        let mut loader = Self {
144            stdlib_path: Self::default_stdlib_path(),
145            module_paths: Self::default_module_paths(),
146            current_project_root: None,
147            cache: ModuleCache::new(),
148            dependency_paths: HashMap::new(),
149            extension_resolver: InMemoryResolver::default(),
150            bundle_resolver: InMemoryResolver::default(),
151            embedded_stdlib_resolver: InMemoryResolver::default(),
152            keychain: None,
153            blob_store: None,
154        };
155
156        // Add paths from SHAPE_PATH environment variable
157        if let Ok(shape_path) = std::env::var("SHAPE_PATH") {
158            for path in shape_path.split(':') {
159                loader.add_module_path(PathBuf::from(path));
160            }
161        }
162
163        for (module_path, source) in EMBEDDED_STDLIB_MODULES {
164            loader.register_embedded_stdlib_module(
165                (*module_path).to_string(),
166                ModuleCode::Source(Arc::from(*source)),
167            );
168        }
169
170        loader
171    }
172
173    /// Clone loader configuration (search paths + resolver payloads) without cache state.
174    pub fn clone_without_cache(&self) -> Self {
175        Self {
176            stdlib_path: self.stdlib_path.clone(),
177            module_paths: self.module_paths.clone(),
178            current_project_root: self.current_project_root.clone(),
179            cache: ModuleCache::new(),
180            dependency_paths: self.dependency_paths.clone(),
181            extension_resolver: self.extension_resolver.clone(),
182            bundle_resolver: self.bundle_resolver.clone(),
183            embedded_stdlib_resolver: self.embedded_stdlib_resolver.clone(),
184            keychain: None,
185            blob_store: self.blob_store.clone(),
186        }
187    }
188
189    /// Get the canonical stdlib path.
190    fn default_stdlib_path() -> PathBuf {
191        crate::stdlib_metadata::default_stdlib_path()
192    }
193
194    /// Get default module search paths
195    fn default_module_paths() -> Vec<PathBuf> {
196        let mut paths = vec![];
197
198        // Current directory
199        paths.push(PathBuf::from("."));
200
201        // Project-specific paths
202        paths.push(PathBuf::from(".shape"));
203        paths.push(PathBuf::from("shape_modules"));
204        paths.push(PathBuf::from("modules"));
205
206        // User home directory paths
207        if let Some(home) = dirs::home_dir() {
208            paths.push(home.join(".shape/modules"));
209            paths.push(home.join(".local/share/shape/modules"));
210        }
211
212        // System-wide paths
213        paths.push(PathBuf::from("/usr/local/share/shape/modules"));
214        paths.push(PathBuf::from("/usr/share/shape/modules"));
215
216        paths
217    }
218
219    /// Add a module search path
220    pub fn add_module_path(&mut self, path: PathBuf) {
221        if !self.module_paths.contains(&path) {
222            self.module_paths.push(path);
223        }
224    }
225
226    /// Set the project root and prepend its configured module paths
227    ///
228    /// Inserts the project root directory itself plus any extra paths
229    /// (typically resolved from shape.toml [modules].paths) at the
230    /// front of the search list so project modules take priority.
231    pub fn set_project_root(&mut self, root: &std::path::Path, extra_paths: &[PathBuf]) {
232        let root_buf = root.to_path_buf();
233        self.current_project_root = Some(root_buf.clone());
234        // Insert project root first, then extra paths, all at front
235        let mut to_prepend = vec![root_buf];
236        to_prepend.extend(extra_paths.iter().cloned());
237        // Remove duplicates from existing paths, then prepend
238        self.module_paths.retain(|p| !to_prepend.contains(p));
239        to_prepend.extend(self.module_paths.drain(..));
240        self.module_paths = to_prepend;
241    }
242
243    /// Configure module paths and dependency paths from workspace/file context.
244    pub fn configure_for_context(&mut self, current_file: &Path, workspace_root: Option<&Path>) {
245        if let Some(project) = resolve_project_root(current_file, workspace_root) {
246            let module_paths = project.resolved_module_paths();
247            self.set_project_root(&project.root_path, &module_paths);
248            self.set_dependency_paths(resolve_path_dependencies(&project));
249        }
250    }
251
252    /// Configure module loader for context and register declared extension artifacts.
253    ///
254    /// This is the canonical context setup path for tooling (LSP/CLI) so
255    /// extension module namespaces are resolved through the same loader.
256    pub fn configure_for_context_with_source(
257        &mut self,
258        current_file: &Path,
259        workspace_root: Option<&Path>,
260        current_source: Option<&str>,
261    ) {
262        self.configure_for_context(current_file, workspace_root);
263        crate::extension_context::register_declared_extensions_in_loader(
264            self,
265            Some(current_file),
266            workspace_root,
267            current_source,
268        );
269    }
270
271    /// Register resolved dependency paths from `[dependencies]` in shape.toml.
272    ///
273    /// Each entry maps a package name to its resolved local path. When a module
274    /// import matches a dependency name, the loader searches that path first.
275    /// If a dependency path points to a `.shapec` bundle file, the bundle is
276    /// loaded and its modules are registered in the bundle resolver.
277    pub fn set_dependency_paths(&mut self, deps: HashMap<String, PathBuf>) {
278        let mut regular_deps = HashMap::new();
279
280        for (name, path) in deps {
281            if path.extension().and_then(|e| e.to_str()) == Some("shapec") && path.is_file() {
282                // Load the bundle and register its modules
283                match crate::package_bundle::PackageBundle::read_from_file(&path) {
284                    Ok(bundle) => {
285                        self.load_bundle(&bundle, Some(&name));
286                    }
287                    Err(e) => {
288                        eprintln!(
289                            "Warning: failed to load bundle dependency '{}' from '{}': {}",
290                            name,
291                            path.display(),
292                            e
293                        );
294                        // Fall back to treating it as a regular path
295                        regular_deps.insert(name, path);
296                    }
297                }
298            } else {
299                regular_deps.insert(name, path);
300            }
301        }
302
303        self.dependency_paths = regular_deps;
304    }
305
306    /// Register an extension-provided in-memory module artifact.
307    pub fn register_extension_module(&mut self, module_path: impl Into<String>, code: ModuleCode) {
308        self.extension_resolver.register(module_path, code);
309    }
310
311    /// Register an embedded stdlib in-memory module artifact.
312    pub fn register_embedded_stdlib_module(
313        &mut self,
314        module_path: impl Into<String>,
315        code: ModuleCode,
316    ) {
317        self.embedded_stdlib_resolver.register(module_path, code);
318    }
319
320    /// Register modules from a package bundle, optionally prefixed with a dependency name.
321    ///
322    /// If the bundle contains content-addressed manifests (v2+), those are
323    /// registered as `ContentAddressed` modules. Otherwise, legacy compiled
324    /// modules are registered as `Compiled`.
325    pub fn load_bundle(
326        &mut self,
327        bundle: &crate::package_bundle::PackageBundle,
328        prefix: Option<&str>,
329    ) {
330        // Register content-addressed modules from manifests (v2 bundles).
331        for manifest in &bundle.manifests {
332            let path = if let Some(prefix) = prefix {
333                format!("{}::{}", prefix, manifest.name)
334            } else {
335                manifest.name.clone()
336            };
337
338            // Collect all blobs referenced by this manifest, including
339            // transitive dependencies from the dependency closure.
340            let mut module_blobs = HashMap::new();
341            for hash in manifest.exports.values() {
342                if let Some(data) = bundle.blob_store.get(hash) {
343                    module_blobs.insert(*hash, data.clone());
344                }
345                // Also include transitive dependencies from the closure.
346                if let Some(deps) = manifest.dependency_closure.get(hash) {
347                    for dep_hash in deps {
348                        if let Some(data) = bundle.blob_store.get(dep_hash) {
349                            module_blobs.insert(*dep_hash, data.clone());
350                        }
351                    }
352                }
353            }
354            for hash in manifest.type_schemas.values() {
355                if let Some(data) = bundle.blob_store.get(hash) {
356                    module_blobs.insert(*hash, data.clone());
357                }
358            }
359
360            self.register_content_addressed_module(path, manifest, module_blobs);
361        }
362
363        // Also register legacy compiled modules.
364        for module in &bundle.modules {
365            let path = if let Some(prefix) = prefix {
366                if module.module_path.is_empty() {
367                    prefix.to_string()
368                } else {
369                    format!("{}::{}", prefix, module.module_path)
370                }
371            } else {
372                module.module_path.clone()
373            };
374
375            self.bundle_resolver.register(
376                path,
377                ModuleCode::Compiled(Arc::from(module.bytecode_bytes.clone().into_boxed_slice())),
378            );
379        }
380    }
381
382    /// Register a content-addressed module from a manifest and its blob data.
383    ///
384    /// The manifest describes the module's exports and type schemas, each
385    /// identified by a content hash. The `blobs` map provides pre-fetched
386    /// blob data keyed by hash so the loader doesn't need to hit a remote
387    /// store.
388    pub fn register_content_addressed_module(
389        &mut self,
390        module_path: impl Into<String>,
391        manifest: &crate::module_manifest::ModuleManifest,
392        blobs: HashMap<[u8; 32], Vec<u8>>,
393    ) {
394        let manifest_bytes =
395            rmp_serde::to_vec(manifest).expect("ModuleManifest serialization should not fail");
396        self.bundle_resolver.register(
397            module_path,
398            ModuleCode::ContentAddressed {
399                manifest_bytes: Arc::from(manifest_bytes.into_boxed_slice()),
400                blob_cache: Arc::new(blobs),
401            },
402        );
403    }
404
405    /// Register bundle modules directly from path/code pairs.
406    pub fn register_bundle_modules(&mut self, modules: Vec<(String, ModuleCode)>) {
407        for (path, code) in modules {
408            self.bundle_resolver.register(path, code);
409        }
410    }
411
412    /// Set an external blob store for lazy-fetching content-addressed blobs
413    /// on cache miss during module loading.
414    pub fn set_blob_store(&mut self, store: Arc<dyn crate::blob_store::BlobStore>) {
415        self.blob_store = Some(store);
416    }
417
418    /// Check whether an extension in-memory module is registered.
419    pub fn has_extension_module(&self, module_path: &str) -> bool {
420        self.extension_resolver.has(module_path)
421    }
422
423    /// List all registered extension in-memory module paths.
424    pub fn extension_module_paths(&self) -> Vec<String> {
425        self.extension_resolver.module_paths()
426    }
427
428    /// List all registered embedded stdlib module paths.
429    pub fn embedded_stdlib_module_paths(&self) -> Vec<String> {
430        self.embedded_stdlib_resolver.module_paths()
431    }
432
433    /// Get the resolved dependency paths.
434    pub fn get_dependency_paths(&self) -> &HashMap<String, PathBuf> {
435        &self.dependency_paths
436    }
437
438    /// Get all module search paths
439    pub fn get_module_paths(&self) -> &[PathBuf] {
440        &self.module_paths
441    }
442
443    /// Get the stdlib path
444    pub fn get_stdlib_path(&self) -> &PathBuf {
445        &self.stdlib_path
446    }
447
448    /// Set the stdlib path
449    pub fn set_stdlib_path(&mut self, path: PathBuf) {
450        self.stdlib_path = path;
451    }
452
453    /// Set the keychain used for module signature verification.
454    ///
455    /// When set, content-addressed modules are verified against the keychain
456    /// before loading. If the keychain requires signatures, unsigned modules
457    /// are rejected.
458    pub fn set_keychain(&mut self, keychain: crate::crypto::Keychain) {
459        self.keychain = Some(keychain);
460    }
461
462    /// Get a reference to the configured keychain, if any.
463    pub fn keychain(&self) -> Option<&crate::crypto::Keychain> {
464        self.keychain.as_ref()
465    }
466
467    /// Clear all module search paths (except stdlib)
468    pub fn clear_module_paths(&mut self) {
469        self.module_paths.clear();
470    }
471
472    /// Reset module paths to defaults
473    pub fn reset_module_paths(&mut self) {
474        self.module_paths = Self::default_module_paths();
475    }
476
477    /// Load a module by path
478    pub fn load_module(&mut self, module_path: &str) -> Result<Arc<Module>> {
479        self.load_module_with_context(module_path, None)
480    }
481
482    /// Resolve a module path to an absolute file path.
483    pub fn resolve_module_path(&self, module_path: &str) -> Result<PathBuf> {
484        self.resolve_module_path_with_context(module_path, None)
485    }
486
487    /// Resolve a module path with an optional importer context directory.
488    pub fn resolve_module_path_with_context(
489        &self,
490        module_path: &str,
491        context_path: Option<&PathBuf>,
492    ) -> Result<PathBuf> {
493        resolve_module_path_with_settings(
494            module_path,
495            context_path.map(|p| p.as_path()),
496            self.stdlib_path.as_path(),
497            &self.module_paths,
498            &self.dependency_paths,
499        )
500    }
501
502    fn load_module_from_resolved_path(
503        &mut self,
504        cache_key: String,
505        compile_module_path: &str,
506        file_path: PathBuf,
507    ) -> Result<Arc<Module>> {
508        let content = std::fs::read_to_string(&file_path).map_err(|e| ShapeError::ModuleError {
509            message: format!("Failed to read module file: {}: {}", file_path.display(), e),
510            module_path: Some(file_path.clone()),
511        })?;
512
513        // Parse the module
514        let ast = parse_program(&content).map_err(|e| ShapeError::ModuleError {
515            message: format!("Failed to parse module: {}: {}", compile_module_path, e),
516            module_path: None,
517        })?;
518        let mut ast = ast;
519        annotate_program_declaring_module_path(&mut ast, compile_module_path);
520        annotate_program_native_abi_package_key(
521            &mut ast,
522            self.package_key_for_origin_path(Some(&file_path))
523                .as_deref(),
524        );
525
526        // Process imports to track dependencies
527        let dependencies = resolution::extract_dependencies(&ast);
528        self.cache
529            .store_dependencies(cache_key.clone(), dependencies.clone());
530
531        // Load all dependencies first (with context of current module's directory)
532        let module_dir = file_path.parent().map(|p| p.to_path_buf());
533        for dep in &dependencies {
534            self.load_module_with_context(dep, module_dir.as_ref())?;
535        }
536
537        // Compile the module
538        let module = loading::compile_module(compile_module_path, ast)?;
539        let module = Arc::new(module);
540
541        // Cache it
542        self.cache.insert(cache_key, module.clone());
543
544        Ok(module)
545    }
546
547    fn load_module_from_source_artifact(
548        &mut self,
549        cache_key: String,
550        compile_module_path: &str,
551        source: &str,
552        origin_path: Option<PathBuf>,
553        context_path: Option<&PathBuf>,
554    ) -> Result<Arc<Module>> {
555        // Parse the module
556        let ast = parse_program(source).map_err(|e| ShapeError::ModuleError {
557            message: format!("Failed to parse module: {}: {}", compile_module_path, e),
558            module_path: origin_path.clone(),
559        })?;
560        let mut ast = ast;
561        annotate_program_declaring_module_path(&mut ast, compile_module_path);
562        annotate_program_native_abi_package_key(
563            &mut ast,
564            self.package_key_for_origin_path(origin_path.as_deref())
565                .as_deref(),
566        );
567
568        // Process imports to track dependencies
569        let dependencies = resolution::extract_dependencies(&ast);
570        self.cache
571            .store_dependencies(cache_key.clone(), dependencies.clone());
572
573        // Load all dependencies first (with best available context directory).
574        let module_dir = origin_path
575            .as_ref()
576            .and_then(|path| path.parent().map(|p| p.to_path_buf()))
577            .or_else(|| context_path.cloned());
578        for dep in &dependencies {
579            self.load_module_with_context(dep, module_dir.as_ref())?;
580        }
581
582        // Compile the module
583        let module = loading::compile_module(compile_module_path, ast)?;
584        let module = Arc::new(module);
585
586        // Cache it
587        self.cache.insert(cache_key, module.clone());
588
589        Ok(module)
590    }
591
592    fn resolve_module_artifact_with_context(
593        &self,
594        module_path: &str,
595        context_path: Option<&PathBuf>,
596    ) -> Result<ResolvedModuleArtifact> {
597        let context = context_path.map(|p| p.as_path());
598
599        if let Some(artifact) = self.extension_resolver.resolve(module_path, context)? {
600            return Ok(artifact);
601        }
602
603        // Check bundle resolver (compiled bundle modules)
604        if let Some(artifact) = self.bundle_resolver.resolve(module_path, context)? {
605            return Ok(artifact);
606        }
607
608        if let Some(artifact) = self
609            .embedded_stdlib_resolver
610            .resolve(module_path, context)?
611        {
612            return Ok(artifact);
613        }
614
615        let filesystem = FilesystemResolver {
616            stdlib_path: self.stdlib_path.as_path(),
617            module_paths: &self.module_paths,
618            dependency_paths: &self.dependency_paths,
619        };
620
621        filesystem
622            .resolve(module_path, context)?
623            .ok_or_else(|| ShapeError::ModuleError {
624                message: format!("Module not found: {}", module_path),
625                module_path: None,
626            })
627    }
628
629    /// Load a module with optional context path
630    pub fn load_module_with_context(
631        &mut self,
632        module_path: &str,
633        context_path: Option<&PathBuf>,
634    ) -> Result<Arc<Module>> {
635        // Check cache first
636        if let Some(module) = self.cache.get(module_path) {
637            return Ok(module);
638        }
639
640        // Check for circular dependencies
641        self.cache.check_circular_dependency(module_path)?;
642
643        // Resolve module artifact from chained resolvers.
644        let artifact = self.resolve_module_artifact_with_context(module_path, context_path)?;
645        // Add to loading stack and ensure cleanup even on early error returns.
646        self.cache.push_loading(module_path.to_string());
647        let result = match artifact.code {
648            ModuleCode::Source(source) => self.load_module_from_source_artifact(
649                module_path.to_string(),
650                module_path,
651                source.as_ref(),
652                artifact.origin_path,
653                context_path,
654            ),
655            ModuleCode::Both { source, .. } => self.load_module_from_source_artifact(
656                module_path.to_string(),
657                module_path,
658                source.as_ref(),
659                artifact.origin_path,
660                context_path,
661            ),
662            ModuleCode::Compiled(_compiled) => {
663                // Create a minimal Module for compiled-only artifacts.
664                // The bytecode will be loaded and executed by the VM directly.
665                let module = Module {
666                    name: module_path
667                        .split("::")
668                        .last()
669                        .unwrap_or(module_path)
670                        .to_string(),
671                    path: module_path.to_string(),
672                    exports: HashMap::new(), // VM resolves exports from bytecode at execution time
673                    ast: shape_ast::ast::Program {
674                        items: vec![],
675                        docs: shape_ast::ast::ProgramDocs::default(),
676                    },
677                };
678                let module = Arc::new(module);
679                self.cache.insert(module_path.to_string(), module.clone());
680                Ok(module)
681            }
682            ModuleCode::ContentAddressed {
683                manifest_bytes,
684                blob_cache,
685            } => {
686                // Deserialize the manifest to discover export names.
687                let manifest: crate::module_manifest::ModuleManifest =
688                    rmp_serde::from_slice(&manifest_bytes).map_err(|e| {
689                        ShapeError::ModuleError {
690                            message: format!(
691                                "Failed to deserialize manifest for '{}': {}",
692                                module_path, e
693                            ),
694                            module_path: None,
695                        }
696                    })?;
697
698                // Verify manifest integrity (hash matches content).
699                if !manifest.verify_integrity() {
700                    return Err(ShapeError::ModuleError {
701                        message: format!(
702                            "Manifest integrity check failed for '{}': content hash mismatch",
703                            module_path
704                        ),
705                        module_path: None,
706                    });
707                }
708
709                // Verify signature against keychain when configured.
710                if let Some(keychain) = &self.keychain {
711                    let sig_data =
712                        manifest
713                            .signature
714                            .as_ref()
715                            .map(|sig| crate::crypto::ModuleSignatureData {
716                                author_key: sig.author_key,
717                                signature: sig.signature.clone(),
718                                signed_at: sig.signed_at,
719                            });
720                    let result = keychain.verify_module(
721                        &manifest.name,
722                        &manifest.manifest_hash,
723                        sig_data.as_ref(),
724                    );
725                    if let crate::crypto::VerifyResult::Rejected(reason) = result {
726                        return Err(ShapeError::ModuleError {
727                            message: format!(
728                                "Signature verification failed for '{}': {}",
729                                module_path, reason
730                            ),
731                            module_path: None,
732                        });
733                    }
734                }
735
736                // Build an exports map from the manifest. The actual blobs
737                // are resolved lazily by the VM via the blob cache / blob store.
738                // For the runtime's Module representation, we record export
739                // names so import resolution can verify symbol existence.
740                let mut exports = HashMap::new();
741                for export_name in manifest.exports.keys() {
742                    // Register a placeholder function export. The VM will
743                    // resolve the real blob at execution time using the hash.
744                    let placeholder_fn = shape_ast::ast::FunctionDef {
745                        name: export_name.clone(),
746                        name_span: shape_ast::ast::Span::default(),
747                        declaring_module_path: None,
748                        doc_comment: None,
749                        params: vec![],
750                        body: vec![],
751                        return_type: None,
752                        is_async: false,
753                        is_comptime: false,
754                        type_params: None,
755                        where_clause: None,
756                        annotations: vec![],
757                    };
758                    exports.insert(
759                        export_name.clone(),
760                        Export::Function(Arc::new(placeholder_fn)),
761                    );
762                }
763
764                // Store the blob cache entries into the bundle resolver so
765                // downstream loaders (VM) can fetch them by hash.
766                for (hash, data) in blob_cache.iter() {
767                    let hex_key = format!("__blob__{}", hex::encode(hash));
768                    self.bundle_resolver.register(
769                        hex_key,
770                        ModuleCode::Compiled(Arc::from(data.clone().into_boxed_slice())),
771                    );
772                }
773
774                // Fetch any missing blobs from the external BlobStore,
775                // including transitive dependencies from the dependency closure.
776                if let Some(ref store) = self.blob_store {
777                    for (_name, hash) in manifest.exports.iter() {
778                        let all_hashes: Vec<&[u8; 32]> = std::iter::once(hash)
779                            .chain(
780                                manifest
781                                    .dependency_closure
782                                    .get(hash)
783                                    .into_iter()
784                                    .flat_map(|v| v.iter()),
785                            )
786                            .collect();
787                        for h in all_hashes {
788                            let hex_key = format!("__blob__{}", hex::encode(h));
789                            if !self.bundle_resolver.has(&hex_key) {
790                                if let Some(data) = store.get(h) {
791                                    self.bundle_resolver.register(
792                                        hex_key,
793                                        ModuleCode::Compiled(Arc::from(data.into_boxed_slice())),
794                                    );
795                                }
796                            }
797                        }
798                    }
799                }
800
801                let module = Module {
802                    name: manifest.name.clone(),
803                    path: module_path.to_string(),
804                    exports,
805                    ast: shape_ast::ast::Program {
806                        items: vec![],
807                        docs: shape_ast::ast::ProgramDocs::default(),
808                    },
809                };
810                let module = Arc::new(module);
811                self.cache.insert(module_path.to_string(), module.clone());
812                Ok(module)
813            }
814        };
815        self.cache.pop_loading();
816        result
817    }
818
819    /// Load and compile a module directly from an absolute/relative file path.
820    ///
821    /// Uses the same parsing/export/dependency logic as `load_module(...)`,
822    /// but keys the cache by canonical file path.
823    pub fn load_module_from_file(&mut self, file_path: &Path) -> Result<Arc<Module>> {
824        let canonical = file_path
825            .canonicalize()
826            .unwrap_or_else(|_| file_path.to_path_buf());
827        let cache_key = canonical.to_string_lossy().to_string();
828
829        // Check cache first
830        if let Some(module) = self.cache.get(&cache_key) {
831            return Ok(module);
832        }
833
834        // Check for circular dependency
835        self.cache.check_circular_dependency(&cache_key)?;
836
837        self.cache.push_loading(cache_key.clone());
838        let result = self.load_module_from_resolved_path(cache_key.clone(), &cache_key, canonical);
839        self.cache.pop_loading();
840
841        result
842    }
843
844    /// List all `std::core::...` import paths available in the configured stdlib.
845    pub fn list_core_stdlib_module_imports(&self) -> Result<Vec<String>> {
846        let mut embedded: Vec<String> = self
847            .embedded_stdlib_resolver
848            .module_paths()
849            .into_iter()
850            .filter(|name| name.starts_with("std::core::"))
851            .collect();
852        if !embedded.is_empty() {
853            embedded.sort();
854            embedded.dedup();
855            return Ok(embedded);
856        }
857
858        if !self.stdlib_path.exists() || !self.stdlib_path.is_dir() {
859            return Err(ShapeError::ModuleError {
860                message: format!(
861                    "Could not find stdlib directory at {}",
862                    self.stdlib_path.display()
863                ),
864                module_path: Some(self.stdlib_path.clone()),
865            });
866        }
867
868        resolution::list_core_stdlib_module_imports(self.stdlib_path.as_path())
869    }
870
871    /// List all `std::...` import paths available in the configured stdlib.
872    pub fn list_stdlib_module_imports(&self) -> Result<Vec<String>> {
873        let mut embedded: Vec<String> = self
874            .embedded_stdlib_resolver
875            .module_paths()
876            .into_iter()
877            .filter(|name| name.starts_with("std::"))
878            .collect();
879        if !embedded.is_empty() {
880            embedded.sort();
881            embedded.dedup();
882            return Ok(embedded);
883        }
884
885        if !self.stdlib_path.exists() || !self.stdlib_path.is_dir() {
886            return Err(ShapeError::ModuleError {
887                message: format!(
888                    "Could not find stdlib directory at {}",
889                    self.stdlib_path.display()
890                ),
891                module_path: Some(self.stdlib_path.clone()),
892            });
893        }
894
895        resolution::list_stdlib_module_imports(self.stdlib_path.as_path())
896    }
897
898    /// List all importable modules for a given workspace/file context.
899    ///
900    /// Includes:
901    /// - `std::...` modules from stdlib
902    /// - project root modules
903    /// - `[modules].paths` entries from `shape.toml`
904    /// - path dependencies from `shape.toml` (`[dependencies]`)
905    /// - local fallback modules near `current_file` when outside a project
906    pub fn list_importable_modules_with_context(
907        &self,
908        current_file: &Path,
909        workspace_root: Option<&Path>,
910    ) -> Vec<String> {
911        let mut modules = self.list_stdlib_module_imports().unwrap_or_default();
912
913        modules.extend(self.embedded_stdlib_resolver.module_paths());
914        modules.extend(self.extension_resolver.module_paths());
915
916        if let Some(project) = resolve_project_root(current_file, workspace_root) {
917            modules.extend(
918                resolution::list_modules_from_root(&project.root_path, None).unwrap_or_default(),
919            );
920
921            for module_path in project.resolved_module_paths() {
922                modules.extend(
923                    resolution::list_modules_from_root(&module_path, None).unwrap_or_default(),
924                );
925            }
926
927            for (dep_name, dep_root) in resolve_path_dependencies(&project) {
928                modules.extend(
929                    resolution::list_modules_from_root(&dep_root, Some(dep_name.as_str()))
930                        .unwrap_or_default(),
931                );
932            }
933        } else if let Some(context_dir) = current_file.parent() {
934            modules
935                .extend(resolution::list_modules_from_root(context_dir, None).unwrap_or_default());
936        }
937
938        modules.sort();
939        modules.dedup();
940        modules.retain(|m| !m.is_empty());
941        modules
942    }
943
944    /// Load `std::core::...` modules via the canonical module resolution pipeline.
945    pub fn load_core_stdlib_modules(&mut self) -> Result<Vec<Arc<Module>>> {
946        let mut modules = Vec::new();
947        for import_path in self.list_core_stdlib_module_imports()? {
948            modules.push(self.load_module(&import_path)?);
949        }
950        Ok(modules)
951    }
952
953    /// Load the standard library modules
954    pub fn load_stdlib(&mut self) -> Result<()> {
955        let _ = self.load_core_stdlib_modules()?;
956        Ok(())
957    }
958
959    /// Get all loaded modules
960    pub fn loaded_modules(&self) -> Vec<&str> {
961        self.cache.loaded_modules()
962    }
963
964    /// Get a specific export from a module
965    pub fn get_export(&self, module_path: &str, export_name: &str) -> Option<&Export> {
966        self.cache.get_export(module_path, export_name)
967    }
968
969    /// Get a module by path
970    pub fn get_module(&self, module_path: &str) -> Option<&Arc<Module>> {
971        self.cache.get_module(module_path)
972    }
973
974    /// Resolve an import statement to actual exports
975    pub fn resolve_import(&mut self, import_stmt: &ImportStmt) -> Result<HashMap<String, Export>> {
976        let module = self.load_module(&import_stmt.from)?;
977        cache::resolve_import(import_stmt, &module)
978    }
979
980    /// Clear the module cache
981    pub fn clear_cache(&mut self) {
982        self.cache.clear();
983    }
984
985    /// Get module dependencies
986    pub fn get_dependencies(&self, module_path: &str) -> Option<&Vec<String>> {
987        self.cache.get_dependencies(module_path)
988    }
989
990    /// Get all module dependencies recursively
991    pub fn get_all_dependencies(&self, module_path: &str) -> Vec<String> {
992        self.cache.get_all_dependencies(module_path)
993    }
994
995    fn package_key_for_origin_path(&self, origin_path: Option<&Path>) -> Option<String> {
996        let origin_path = origin_path?;
997        let origin = origin_path
998            .canonicalize()
999            .unwrap_or_else(|_| origin_path.to_path_buf());
1000
1001        for dep_root in self.dependency_paths.values() {
1002            let dep_root = dep_root.canonicalize().unwrap_or_else(|_| dep_root.clone());
1003            if origin.starts_with(&dep_root)
1004                && let Some(project) = find_project_root(&dep_root)
1005            {
1006                return Some(normalize_package_identity(&project.root_path, &project.config).2);
1007            }
1008        }
1009
1010        if let Some(project_root) = &self.current_project_root {
1011            let project_root = project_root
1012                .canonicalize()
1013                .unwrap_or_else(|_| project_root.clone());
1014            if origin.starts_with(&project_root)
1015                && let Some(project) = find_project_root(&project_root)
1016            {
1017                return Some(normalize_package_identity(&project.root_path, &project.config).2);
1018            }
1019        }
1020
1021        None
1022    }
1023}
1024
1025fn annotate_program_native_abi_package_key(program: &mut Program, package_key: Option<&str>) {
1026    let Some(package_key) = package_key else {
1027        return;
1028    };
1029    for item in &mut program.items {
1030        annotate_item_native_abi_package_key(item, package_key);
1031    }
1032}
1033
1034fn annotate_program_declaring_module_path(program: &mut Program, module_path: &str) {
1035    for item in &mut program.items {
1036        annotate_item_declaring_module_path(item, module_path);
1037    }
1038}
1039
1040fn annotate_item_native_abi_package_key(item: &mut shape_ast::ast::Item, package_key: &str) {
1041    use shape_ast::ast::{ExportItem, Item};
1042
1043    match item {
1044        Item::ForeignFunction(def, _) => {
1045            if let Some(native) = def.native_abi.as_mut()
1046                && native.package_key.is_none()
1047            {
1048                native.package_key = Some(package_key.to_string());
1049            }
1050        }
1051        Item::Export(export, _) => {
1052            if let ExportItem::ForeignFunction(def) = &mut export.item
1053                && let Some(native) = def.native_abi.as_mut()
1054                && native.package_key.is_none()
1055            {
1056                native.package_key = Some(package_key.to_string());
1057            }
1058        }
1059        Item::Module(module, _) => {
1060            for nested in &mut module.items {
1061                annotate_item_native_abi_package_key(nested, package_key);
1062            }
1063        }
1064        _ => {}
1065    }
1066}
1067
1068fn annotate_item_declaring_module_path(item: &mut shape_ast::ast::Item, module_path: &str) {
1069    use shape_ast::ast::{ExportItem, Item};
1070
1071    match item {
1072        Item::Function(def, _) => {
1073            if def.declaring_module_path.is_none() {
1074                def.declaring_module_path = Some(module_path.to_string());
1075            }
1076        }
1077        Item::Export(export, _) => match &mut export.item {
1078            ExportItem::Function(def) => {
1079                if def.declaring_module_path.is_none() {
1080                    def.declaring_module_path = Some(module_path.to_string());
1081                }
1082            }
1083            ExportItem::ForeignFunction(_) => {}
1084            _ => {}
1085        },
1086        Item::Extend(extend, _) => {
1087            for method in &mut extend.methods {
1088                if method.declaring_module_path.is_none() {
1089                    method.declaring_module_path = Some(module_path.to_string());
1090                }
1091            }
1092        }
1093        Item::Impl(impl_block, _) => {
1094            for method in &mut impl_block.methods {
1095                if method.declaring_module_path.is_none() {
1096                    method.declaring_module_path = Some(module_path.to_string());
1097                }
1098            }
1099        }
1100        Item::Module(module, _) => {
1101            let nested_path = format!("{}::{}", module_path, module.name);
1102            for nested in &mut module.items {
1103                annotate_item_declaring_module_path(nested, &nested_path);
1104            }
1105        }
1106        _ => {}
1107    }
1108}
1109
1110impl Default for ModuleLoader {
1111    fn default() -> Self {
1112        Self::new()
1113    }
1114}
1115
1116/// Canonical module resolution entrypoint shared by runtime, VM, and tooling.
1117pub fn resolve_module_path_with_settings(
1118    module_path: &str,
1119    context_path: Option<&Path>,
1120    stdlib_path: &Path,
1121    module_paths: &[PathBuf],
1122    dependency_paths: &HashMap<String, PathBuf>,
1123) -> Result<PathBuf> {
1124    resolution::resolve_module_path_with_context(
1125        module_path,
1126        context_path,
1127        stdlib_path,
1128        module_paths,
1129        dependency_paths,
1130    )
1131}
1132
1133fn resolve_project_root(current_file: &Path, workspace_root: Option<&Path>) -> Option<ProjectRoot> {
1134    workspace_root
1135        .and_then(find_project_root)
1136        .or_else(|| current_file.parent().and_then(find_project_root))
1137}
1138
1139fn resolve_path_dependencies(project: &ProjectRoot) -> HashMap<String, PathBuf> {
1140    let mut resolved = HashMap::new();
1141
1142    for (name, spec) in &project.config.dependencies {
1143        if let DependencySpec::Detailed(detailed) = spec {
1144            if let Some(path) = &detailed.path {
1145                let dep_path = project.root_path.join(path);
1146                let canonical = dep_path.canonicalize().unwrap_or(dep_path);
1147                resolved.insert(name.clone(), canonical);
1148            }
1149        }
1150    }
1151
1152    resolved
1153}
1154
1155#[cfg(test)]
1156mod tests {
1157    use super::*;
1158    use std::sync::Arc;
1159
1160    #[test]
1161    fn test_compile_module_exports_function() {
1162        let source = r#"
1163pub fn greet(name) {
1164    return "Hello, " + name
1165}
1166"#;
1167        let ast = parse_program(source).unwrap();
1168        let module = loading::compile_module("test_module", ast).unwrap();
1169
1170        assert!(
1171            module.exports.contains_key("greet"),
1172            "Expected 'greet' export, got: {:?}",
1173            module.exports.keys().collect::<Vec<_>>()
1174        );
1175
1176        match module.exports.get("greet") {
1177            Some(Export::Function(func)) => {
1178                assert_eq!(func.name, "greet");
1179            }
1180            other => panic!("Expected Function export, got: {:?}", other),
1181        }
1182    }
1183
1184    #[test]
1185    fn test_collect_exported_function_names_from_source() {
1186        let source = r#"
1187fn hidden() { 0 }
1188pub fn connect(uri) { uri }
1189pub fn ping() { 1 }
1190"#;
1191        let names = collect_exported_function_names_from_source("duckdb", source)
1192            .expect("should collect exported functions");
1193        assert_eq!(names, vec!["connect".to_string(), "ping".to_string()]);
1194    }
1195
1196    #[test]
1197    fn test_stdlib_methods_are_annotated_with_declaring_module_path() {
1198        let mut loader = ModuleLoader::new();
1199        let module = loader
1200            .load_module("std::core::json_value")
1201            .expect("load stdlib module");
1202
1203        let extend = module
1204            .ast
1205            .items
1206            .iter()
1207            .find_map(|item| match item {
1208                shape_ast::ast::Item::Extend(extend, _) => Some(extend),
1209                _ => None,
1210            })
1211            .expect("json_value module should contain an extend block");
1212        let method = extend
1213            .methods
1214            .iter()
1215            .find(|method| method.name == "get")
1216            .expect("json_value extend block should contain get()");
1217
1218        assert_eq!(
1219            method.declaring_module_path.as_deref(),
1220            Some("std::core::json_value")
1221        );
1222    }
1223
1224    #[test]
1225    fn test_load_module_from_temp_file() {
1226        use std::io::Write;
1227
1228        // Create a temp file with a module
1229        let temp_dir = std::env::temp_dir();
1230        let module_path = temp_dir.join("test_load_module.shape");
1231        let mut file = std::fs::File::create(&module_path).unwrap();
1232        writeln!(
1233            file,
1234            r#"
1235pub fn add(a, b) {{
1236    return a + b
1237}}
1238"#
1239        )
1240        .unwrap();
1241
1242        // Create loader and add temp dir to search paths
1243        let mut loader = ModuleLoader::new();
1244        loader.add_module_path(temp_dir.clone());
1245
1246        // Load the module via search path (relative imports no longer supported)
1247        let result = loader.load_module_with_context("test_load_module", Some(&temp_dir));
1248
1249        // Clean up
1250        std::fs::remove_file(&module_path).ok();
1251
1252        // Verify
1253        let module = result.expect("Module should load");
1254        assert!(
1255            module.exports.contains_key("add"),
1256            "Expected 'add' export, got: {:?}",
1257            module.exports.keys().collect::<Vec<_>>()
1258        );
1259    }
1260
1261    #[test]
1262    fn test_load_module_from_file_path() {
1263        use std::io::Write;
1264
1265        let temp_dir = tempfile::tempdir().expect("temp dir");
1266        let module_path = temp_dir.path().join("helpers.shape");
1267        let mut file = std::fs::File::create(&module_path).expect("create module");
1268        writeln!(
1269            file,
1270            r#"
1271pub fn helper(x) {{
1272    x
1273}}
1274"#
1275        )
1276        .expect("write module");
1277
1278        let mut loader = ModuleLoader::new();
1279        let module = loader
1280            .load_module_from_file(&module_path)
1281            .expect("module should load from file path");
1282        assert!(
1283            module.exports.contains_key("helper"),
1284            "Expected 'helper' export, got: {:?}",
1285            module.exports.keys().collect::<Vec<_>>()
1286        );
1287    }
1288
1289    #[test]
1290    fn test_loaded_dependency_module_annotates_native_abi_with_package_key() {
1291        let root = tempfile::tempdir().expect("tempdir");
1292        let dep_root = root.path().join("dep_pkg");
1293
1294        std::fs::create_dir_all(&dep_root).expect("create dep root");
1295        std::fs::write(
1296            root.path().join("shape.toml"),
1297            r#"
1298[project]
1299name = "app"
1300version = "0.1.0"
1301
1302[dependencies]
1303dep_pkg = { path = "./dep_pkg" }
1304"#,
1305        )
1306        .expect("write root shape.toml");
1307        std::fs::write(
1308            dep_root.join("shape.toml"),
1309            r#"
1310[project]
1311name = "dep_pkg"
1312version = "1.2.3"
1313"#,
1314        )
1315        .expect("write dep shape.toml");
1316        std::fs::write(
1317            dep_root.join("index.shape"),
1318            r#"
1319extern C fn dep_call() -> i32 from "shared";
1320"#,
1321        )
1322        .expect("write dep source");
1323
1324        let mut loader = ModuleLoader::new();
1325        loader.set_project_root(root.path(), &[]);
1326        loader.set_dependency_paths(HashMap::from([("dep_pkg".to_string(), dep_root.clone())]));
1327
1328        let module = loader.load_module("dep_pkg").expect("load dep module");
1329        let foreign = module
1330            .ast
1331            .items
1332            .iter()
1333            .find_map(|item| match item {
1334                shape_ast::ast::Item::ForeignFunction(def, _) => Some(def),
1335                _ => None,
1336            })
1337            .expect("foreign function should exist");
1338        let native = foreign
1339            .native_abi
1340            .as_ref()
1341            .expect("native abi should exist");
1342        assert_eq!(native.package_key.as_deref(), Some("dep_pkg@1.2.3"));
1343    }
1344
1345    #[test]
1346    fn test_collect_exported_symbols_detects_pub_function_and_enum() {
1347        let source = r#"
1348pub fn helper() { 1 }
1349pub enum Side { Buy, Sell }
1350"#;
1351        let ast = parse_program(source).unwrap();
1352        let exports = collect_exported_symbols(&ast).unwrap();
1353
1354        let helper = exports
1355            .iter()
1356            .find(|e| e.name == "helper")
1357            .expect("expected helper export");
1358        assert_eq!(helper.name, "helper");
1359        assert!(helper.alias.is_none());
1360        assert_eq!(helper.kind, ModuleExportKind::Function);
1361
1362        let side = exports
1363            .iter()
1364            .find(|e| e.name == "Side")
1365            .expect("expected Side export");
1366        assert_eq!(side.kind, ModuleExportKind::Enum);
1367    }
1368
1369    #[test]
1370    fn test_list_core_stdlib_module_imports_contains_core_modules() {
1371        let loader = ModuleLoader::new();
1372        let modules = loader
1373            .list_core_stdlib_module_imports()
1374            .expect("should list std.core modules");
1375
1376        assert!(
1377            !modules.is_empty(),
1378            "expected non-empty std.core module list"
1379        );
1380        assert!(
1381            modules.iter().all(|m| m.starts_with("std::core::")),
1382            "expected std::core::* import paths, got: {:?}",
1383            modules
1384        );
1385        assert!(
1386            modules.iter().any(|m| m == "std::core::math"),
1387            "expected std::core::math in core module list"
1388        );
1389    }
1390
1391    #[test]
1392    fn test_list_stdlib_module_imports_includes_non_core_namespaces() {
1393        let loader = ModuleLoader::new();
1394        let modules = loader
1395            .list_stdlib_module_imports()
1396            .expect("should list stdlib modules");
1397
1398        assert!(
1399            modules.iter().any(|m| m.starts_with("std::finance::")),
1400            "expected finance stdlib modules in list, got: {:?}",
1401            modules
1402        );
1403    }
1404
1405    #[test]
1406    fn test_embedded_stdlib_loads_without_filesystem_path() {
1407        let mut loader = ModuleLoader::new();
1408        loader.set_stdlib_path(std::env::temp_dir().join("shape_missing_stdlib_dir"));
1409
1410        let module = loader
1411            .load_module("std::core::snapshot")
1412            .expect("embedded stdlib module should load without filesystem stdlib");
1413        assert!(
1414            module.exports.contains_key("snapshot"),
1415            "expected snapshot export from std::core::snapshot"
1416        );
1417    }
1418
1419    #[test]
1420    fn test_list_importable_modules_with_context_includes_project_and_deps() {
1421        let tmp = tempfile::tempdir().unwrap();
1422        let root = tmp.path();
1423
1424        std::fs::write(
1425            root.join("shape.toml"),
1426            r#"
1427[modules]
1428paths = ["lib"]
1429
1430[dependencies]
1431mydep = { path = "deps/mydep" }
1432"#,
1433        )
1434        .unwrap();
1435
1436        std::fs::create_dir_all(root.join("src")).unwrap();
1437        std::fs::create_dir_all(root.join("lib")).unwrap();
1438        std::fs::create_dir_all(root.join("deps/mydep")).unwrap();
1439
1440        std::fs::write(root.join("src/main.shape"), "let x = 1").unwrap();
1441        std::fs::write(root.join("lib/tools.shape"), "pub fn tool() { 1 }").unwrap();
1442        std::fs::write(root.join("deps/mydep/index.shape"), "pub fn root() { 1 }").unwrap();
1443        std::fs::write(root.join("deps/mydep/util.shape"), "pub fn util() { 1 }").unwrap();
1444
1445        let loader = ModuleLoader::new();
1446        let modules =
1447            loader.list_importable_modules_with_context(&root.join("src/main.shape"), None);
1448
1449        assert!(
1450            modules.iter().any(|m| m == "tools"),
1451            "expected module path from [modules].paths, got: {:?}",
1452            modules
1453        );
1454        assert!(
1455            modules.iter().any(|m| m == "mydep"),
1456            "expected dependency index module path, got: {:?}",
1457            modules
1458        );
1459        assert!(
1460            modules.iter().any(|m| m == "mydep::util"),
1461            "expected dependency submodule path, got: {:?}",
1462            modules
1463        );
1464    }
1465
1466    #[test]
1467    fn test_load_in_memory_extension_module() {
1468        let mut loader = ModuleLoader::new();
1469        loader.register_extension_module(
1470            "duckdb",
1471            ModuleCode::Source(Arc::from(
1472                r#"
1473pub fn connect(uri) { uri }
1474"#,
1475            )),
1476        );
1477
1478        let module = loader
1479            .load_module("duckdb")
1480            .expect("in-memory extension module should load");
1481        assert!(
1482            module.exports.contains_key("connect"),
1483            "expected connect export, got {:?}",
1484            module.exports.keys().collect::<Vec<_>>()
1485        );
1486    }
1487
1488    #[test]
1489    fn test_load_in_memory_extension_module_with_dependency() {
1490        let mut loader = ModuleLoader::new();
1491        loader.register_extension_module(
1492            "b",
1493            ModuleCode::Source(Arc::from(
1494                r#"
1495pub fn answer() { 42 }
1496"#,
1497            )),
1498        );
1499        loader.register_extension_module(
1500            "a",
1501            ModuleCode::Source(Arc::from(
1502                r#"
1503from b use { answer }
1504pub fn use_answer() { answer() }
1505"#,
1506            )),
1507        );
1508
1509        let module = loader
1510            .load_module("a")
1511            .expect("in-memory module with dependency should load");
1512        assert!(
1513            module.exports.contains_key("use_answer"),
1514            "expected use_answer export"
1515        );
1516        assert!(
1517            loader.get_module("b").is_some(),
1518            "dependency module b should load"
1519        );
1520    }
1521
1522    #[test]
1523    fn test_load_bundle_modules() {
1524        use crate::package_bundle::{BundleMetadata, BundledModule, PackageBundle};
1525
1526        let bundle = PackageBundle {
1527            metadata: BundleMetadata {
1528                name: "test".to_string(),
1529                version: "0.1.0".to_string(),
1530                compiler_version: "0.5.0".to_string(),
1531                source_hash: "abc".to_string(),
1532                bundle_kind: "portable-bytecode".to_string(),
1533                build_host: "x86_64-linux".to_string(),
1534                native_portable: true,
1535                entry_module: None,
1536                built_at: 0,
1537                readme: None,
1538            },
1539            modules: vec![BundledModule {
1540                module_path: "helpers".to_string(),
1541                bytecode_bytes: vec![1, 2, 3],
1542                export_names: vec!["helper".to_string()],
1543                source_hash: "def".to_string(),
1544            }],
1545            dependencies: std::collections::HashMap::new(),
1546            blob_store: std::collections::HashMap::new(),
1547            manifests: vec![],
1548            native_dependency_scopes: vec![],
1549            docs: std::collections::HashMap::new(),
1550        };
1551
1552        let mut loader = ModuleLoader::new();
1553        loader.load_bundle(&bundle, Some("mylib"));
1554
1555        // The bundle module should be resolvable
1556        let artifact = loader.resolve_module_artifact_with_context("mylib::helpers", None);
1557        assert!(artifact.is_ok(), "bundle module should be resolvable");
1558    }
1559}