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 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 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, 0,
177 module.exports.iter().cloned().collect(),
178 ));
179 }
180 }
181 Ok(())
182 }
183 }
184 }
185
186 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 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}