sea_core/module/
resolver.rs

1use crate::error::fuzzy::levenshtein_distance;
2use crate::parser::ast::{Ast, AstNode, ImportDecl, ImportSpecifier};
3use crate::parser::{parse_source, ParseError, ParseOptions, ParseResult};
4use crate::registry::{NamespaceBinding, NamespaceRegistry};
5use std::collections::{HashMap, HashSet};
6use std::fs;
7use std::path::{Path, PathBuf};
8
9#[derive(Debug, Clone)]
10pub struct ModuleInfo {
11    pub namespace: String,
12    pub exports: HashSet<String>,
13    pub ast: Ast,
14}
15
16#[derive(Debug)]
17pub struct ModuleResolver<'a> {
18    registry: &'a NamespaceRegistry,
19    bindings: Vec<NamespaceBinding>,
20    loaded_modules: HashMap<PathBuf, ModuleInfo>,
21    visiting: HashSet<PathBuf>,
22}
23
24impl<'a> ModuleResolver<'a> {
25    pub fn new(registry: &'a NamespaceRegistry) -> ParseResult<Self> {
26        let bindings = registry
27            .resolve_files()
28            .map_err(|e| ParseError::GrammarError(e.to_string()))?;
29        Ok(Self {
30            registry,
31            bindings,
32            loaded_modules: HashMap::new(),
33            visiting: HashSet::new(),
34        })
35    }
36
37    pub fn validate_entry(
38        &mut self,
39        entry_path: impl AsRef<Path>,
40        source: &str,
41    ) -> ParseResult<Ast> {
42        let path = entry_path
43            .as_ref()
44            .canonicalize()
45            .unwrap_or_else(|_| entry_path.as_ref().to_path_buf());
46        let ast = parse_source(source)?;
47        self.visit(&path, &ast)?;
48        Ok(ast)
49    }
50
51    pub fn validate_dependencies(
52        &mut self,
53        entry_path: impl AsRef<Path>,
54        ast: &Ast,
55    ) -> ParseResult<()> {
56        let path = entry_path
57            .as_ref()
58            .canonicalize()
59            .unwrap_or_else(|_| entry_path.as_ref().to_path_buf());
60        self.visit(&path, ast)
61    }
62
63    fn visit(&mut self, path: &Path, ast: &Ast) -> ParseResult<()> {
64        let canonical = if path.to_string_lossy().starts_with("__std__") {
65            path.to_path_buf()
66        } else {
67            path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
68        };
69
70        if self.visiting.contains(&canonical) {
71            // Build the cycle path from currently visiting modules
72            let cycle: Vec<String> = self
73                .visiting
74                .iter()
75                .map(|p| p.to_string_lossy().to_string())
76                .chain(std::iter::once(canonical.to_string_lossy().to_string()))
77                .collect();
78            return Err(ParseError::circular_dependency(cycle));
79        }
80        if self.loaded_modules.contains_key(&canonical) {
81            return Ok(());
82        }
83
84        self.visiting.insert(canonical.clone());
85        let namespace = ast.metadata.namespace.clone().unwrap_or_else(|| {
86            self.registry
87                .namespace_for(path)
88                .unwrap_or(self.registry.default_namespace())
89                .to_string()
90        });
91        let exports = collect_exports(ast);
92
93        for import in &ast.metadata.imports {
94            let dep_path = self.resolve_module_path(&import.from_module)?;
95            let dependency_ast = self.parse_file(&dep_path)?;
96            self.visit(&dep_path, &dependency_ast)?;
97            self.validate_import_targets(import, &dep_path)?;
98        }
99
100        self.loaded_modules.insert(
101            canonical.clone(),
102            ModuleInfo {
103                namespace,
104                exports,
105                ast: ast.clone(),
106            },
107        );
108        self.visiting.remove(&canonical);
109        Ok(())
110    }
111
112    fn parse_file(&self, path: &Path) -> ParseResult<Ast> {
113        let path_str = path.to_string_lossy();
114        if path_str.starts_with("__std__") {
115            let namespace = path_str.strip_prefix("__std__").unwrap();
116            let content = match namespace {
117                "std" | "std:core" => include_str!("../../std/core.sea"),
118                "std:http" => include_str!("../../std/http.sea"),
119                "std:aws" => include_str!("../../std/aws.sea"),
120                _ => {
121                    return Err(ParseError::GrammarError(format!(
122                        "Unknown std module: {}",
123                        namespace
124                    )))
125                }
126            };
127            return parse_source(content);
128        }
129
130        let content = fs::read_to_string(path).map_err(|e| {
131            ParseError::GrammarError(format!("Failed to read module {}: {}", path.display(), e))
132        })?;
133        parse_source(&content)
134    }
135
136    fn resolve_module_path(&self, namespace: &str) -> ParseResult<PathBuf> {
137        if namespace == "std" || namespace.starts_with("std:") {
138            return Ok(PathBuf::from(format!("__std__{}", namespace)));
139        }
140
141        self.bindings
142            .iter()
143            .find(|binding| binding.namespace == namespace)
144            .map(|binding| binding.path.clone())
145            .ok_or_else(|| {
146                // Find similar namespace for suggestion
147                let suggestion = self.suggest_similar_namespace(namespace);
148                ParseError::namespace_not_found(namespace, 0, 0, suggestion)
149            })
150    }
151
152    fn validate_import_targets(&self, import: &ImportDecl, dep_path: &Path) -> ParseResult<()> {
153        let canonical = if dep_path.to_string_lossy().starts_with("__std__") {
154            dep_path.to_path_buf()
155        } else {
156            dep_path
157                .canonicalize()
158                .unwrap_or_else(|_| dep_path.to_path_buf())
159        };
160        let module = self.loaded_modules.get(&canonical).ok_or_else(|| {
161            ParseError::GrammarError(format!(
162                "Expected module '{}' to be loaded before validating imports",
163                dep_path.display()
164            ))
165        })?;
166
167        match &import.specifier {
168            ImportSpecifier::Wildcard(_) => Ok(()),
169            ImportSpecifier::Named(items) => {
170                for item in items {
171                    if !module.exports.contains(&item.name) {
172                        return Err(ParseError::symbol_not_exported(
173                            &item.name,
174                            &module.namespace,
175                            0, // TODO: Extract line from import.location if available
176                            0,
177                            module.exports.iter().cloned().collect(),
178                        ));
179                    }
180                }
181                Ok(())
182            }
183        }
184    }
185
186    /// Find a similar namespace name for error suggestions using Levenshtein distance
187    fn suggest_similar_namespace(&self, target: &str) -> Option<String> {
188        let available: Vec<&str> = self.bindings.iter().map(|b| b.namespace.as_str()).collect();
189
190        available
191            .iter()
192            .filter_map(|ns| {
193                let distance = levenshtein_distance(target, ns);
194                if distance <= 2 {
195                    Some((*ns, distance))
196                } else {
197                    None
198                }
199            })
200            .min_by_key(|(_, d)| *d)
201            .map(|(ns, _)| ns.to_string())
202    }
203}
204
205fn collect_exports(ast: &Ast) -> HashSet<String> {
206    let mut exports = HashSet::new();
207    for node in &ast.declarations {
208        if let AstNode::Export(inner) = &node.node {
209            if let Some(name) = declaration_name(&inner.node) {
210                exports.insert(name.to_string());
211            }
212        }
213    }
214    exports
215}
216
217fn declaration_name(node: &AstNode) -> Option<&str> {
218    match node {
219        AstNode::Entity { name, .. }
220        | AstNode::Resource { name, .. }
221        | AstNode::Flow {
222            resource_name: name,
223            ..
224        }
225        | AstNode::Pattern { name, .. }
226        | AstNode::Role { name, .. }
227        | AstNode::Relation { name, .. }
228        | AstNode::Dimension { name }
229        | AstNode::UnitDeclaration { symbol: name, .. }
230        | AstNode::Policy { name, .. }
231        | AstNode::Instance { name, .. }
232        | AstNode::ConceptChange { name, .. }
233        | AstNode::Metric { name, .. }
234        | AstNode::MappingDecl { name, .. }
235        | AstNode::ProjectionDecl { name, .. } => Some(name),
236        AstNode::Export(inner) => declaration_name(&inner.node),
237    }
238}
239
240pub fn parse_with_registry(
241    path: &Path,
242    registry: &NamespaceRegistry,
243) -> ParseResult<(Ast, ParseOptions)> {
244    let content = fs::read_to_string(path).map_err(|e| {
245        ParseError::GrammarError(format!("Failed to read {}: {}", path.display(), e))
246    })?;
247
248    // `ParseOptions` are constructed here to be returned to the caller,
249    // even though `parse_source` currently doesn't use them. Initialize
250    // fields directly in the `ParseOptions` construction to avoid
251    // field reassignment lint from clippy (clippy::field-reassign-with-default).
252    let options = ParseOptions {
253        namespace_registry: Some(registry.clone()),
254        entry_path: Some(path.to_path_buf()),
255        ..Default::default()
256    };
257
258    let ast = parse_source(&content)?;
259    Ok((ast, options))
260}