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