Skip to main content

shape_runtime/module_loader/
resolver.rs

1//! Unified module artifact resolver abstractions.
2//!
3//! Module resolution is origin-agnostic:
4//! - filesystem modules
5//! - embedded stdlib modules
6//! - extension-bundled modules
7
8use shape_ast::error::{Result, ShapeError};
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11use std::sync::Arc;
12
13/// Resolved module payload content.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum ModuleCode {
16    Source(Arc<str>),
17    Compiled(Arc<[u8]>),
18    Both {
19        source: Arc<str>,
20        compiled: Arc<[u8]>,
21    },
22    /// Content-addressed module: exports are resolved by hash through a
23    /// manifest and blobs are fetched from a `BlobStore` on demand.
24    ContentAddressed {
25        /// Serialized `ModuleManifest` (MessagePack).
26        manifest_bytes: Arc<[u8]>,
27        /// Pre-fetched blob cache: content hash -> raw blob bytes.
28        /// Blobs not in this map will be fetched from the ambient blob store.
29        blob_cache: Arc<HashMap<[u8; 32], Vec<u8>>>,
30    },
31}
32
33impl ModuleCode {
34    /// Return source text if present.
35    pub fn source(&self) -> Option<&str> {
36        match self {
37            Self::Source(source) => Some(source),
38            Self::Compiled(_) => None,
39            Self::Both { source, .. } => Some(source),
40            Self::ContentAddressed { .. } => None,
41        }
42    }
43
44    /// Return compiled payload if present.
45    pub fn compiled(&self) -> Option<&[u8]> {
46        match self {
47            Self::Source(_) => None,
48            Self::Compiled(compiled) => Some(compiled),
49            Self::Both { compiled, .. } => Some(compiled),
50            Self::ContentAddressed { .. } => None,
51        }
52    }
53
54    /// Return manifest bytes if this is a content-addressed module.
55    pub fn manifest_bytes(&self) -> Option<&[u8]> {
56        match self {
57            Self::ContentAddressed { manifest_bytes, .. } => Some(manifest_bytes),
58            _ => None,
59        }
60    }
61
62    /// Return the blob cache if this is a content-addressed module.
63    pub fn blob_cache(&self) -> Option<&HashMap<[u8; 32], Vec<u8>>> {
64        match self {
65            Self::ContentAddressed { blob_cache, .. } => Some(blob_cache),
66            _ => None,
67        }
68    }
69}
70
71/// A resolved module artifact with optional filesystem origin metadata.
72#[derive(Debug, Clone)]
73pub struct ResolvedModuleArtifact {
74    pub module_path: String,
75    pub code: ModuleCode,
76    pub origin_path: Option<PathBuf>,
77}
78
79/// Trait implemented by all module resolvers.
80pub trait ModuleResolver {
81    fn resolve(
82        &self,
83        module_path: &str,
84        context_path: Option<&Path>,
85    ) -> Result<Option<ResolvedModuleArtifact>>;
86
87    fn list_modules(&self) -> Result<Vec<String>> {
88        Ok(Vec::new())
89    }
90}
91
92/// In-memory resolver used for embedded stdlib and extension modules.
93#[derive(Debug, Clone, Default)]
94pub struct InMemoryResolver {
95    modules: HashMap<String, ModuleCode>,
96}
97
98impl InMemoryResolver {
99    pub fn register(&mut self, module_path: impl Into<String>, code: ModuleCode) {
100        self.modules.insert(module_path.into(), code);
101    }
102
103    pub fn clear(&mut self) {
104        self.modules.clear();
105    }
106
107    pub fn has(&self, module_path: &str) -> bool {
108        self.modules.contains_key(module_path)
109    }
110
111    pub fn module_paths(&self) -> Vec<String> {
112        let mut items: Vec<String> = self.modules.keys().cloned().collect();
113        items.sort();
114        items
115    }
116}
117
118impl ModuleResolver for InMemoryResolver {
119    fn resolve(
120        &self,
121        module_path: &str,
122        _context_path: Option<&Path>,
123    ) -> Result<Option<ResolvedModuleArtifact>> {
124        Ok(self
125            .modules
126            .get(module_path)
127            .cloned()
128            .map(|code| ResolvedModuleArtifact {
129                module_path: module_path.to_string(),
130                code,
131                origin_path: None,
132            }))
133    }
134
135    fn list_modules(&self) -> Result<Vec<String>> {
136        Ok(self.module_paths())
137    }
138}
139
140/// Filesystem resolver using standard module path rules.
141pub struct FilesystemResolver<'a> {
142    pub stdlib_path: &'a Path,
143    pub module_paths: &'a [PathBuf],
144    pub dependency_paths: &'a HashMap<String, PathBuf>,
145}
146
147impl<'a> ModuleResolver for FilesystemResolver<'a> {
148    fn resolve(
149        &self,
150        module_path: &str,
151        context_path: Option<&Path>,
152    ) -> Result<Option<ResolvedModuleArtifact>> {
153        let resolved = super::resolution::resolve_module_path_with_context(
154            module_path,
155            context_path,
156            self.stdlib_path,
157            self.module_paths,
158            self.dependency_paths,
159        )?;
160
161        let source = std::fs::read_to_string(&resolved).map_err(|e| ShapeError::ModuleError {
162            message: format!("Failed to read module file: {}: {}", resolved.display(), e),
163            module_path: Some(resolved.clone()),
164        })?;
165
166        Ok(Some(ResolvedModuleArtifact {
167            module_path: module_path.to_string(),
168            code: ModuleCode::Source(Arc::from(source)),
169            origin_path: Some(resolved),
170        }))
171    }
172}