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