1use crate::ast::{Include, Program, SourceLocation, UnionDef, WordDef};
12use crate::parser::Parser;
13use crate::stdlib_embed;
14use std::collections::HashSet;
15use std::path::{Path, PathBuf};
16
17pub struct ResolveResult {
19 pub program: Program,
21 pub ffi_includes: Vec<String>,
23 pub source_files: Vec<PathBuf>,
25 pub embedded_modules: Vec<String>,
27}
28
29struct ResolvedContent {
31 words: Vec<WordDef>,
32 unions: Vec<UnionDef>,
33}
34
35#[derive(Debug)]
37enum ResolvedInclude {
38 Embedded(String, &'static str),
40 FilePath(PathBuf),
42}
43
44pub struct Resolver {
46 included_files: HashSet<PathBuf>,
48 included_embedded: HashSet<String>,
50 stdlib_path: Option<PathBuf>,
52 ffi_includes: Vec<String>,
54}
55
56impl Resolver {
57 pub fn new(stdlib_path: Option<PathBuf>) -> Self {
59 Resolver {
60 included_files: HashSet::new(),
61 included_embedded: HashSet::new(),
62 stdlib_path,
63 ffi_includes: Vec::new(),
64 }
65 }
66
67 pub fn resolve(
73 &mut self,
74 source_path: &Path,
75 program: Program,
76 ) -> Result<ResolveResult, String> {
77 let source_path = source_path
78 .canonicalize()
79 .map_err(|e| format!("Failed to canonicalize {}: {}", source_path.display(), e))?;
80
81 self.included_files.insert(source_path.clone());
83
84 let source_dir = source_path.parent().unwrap_or(Path::new("."));
85 let mut all_words = Vec::new();
86 let mut all_unions = Vec::new();
87
88 for mut word in program.words {
89 if let Some(ref mut source) = word.source {
91 source.file = source_path.clone();
92 } else {
93 word.source = Some(SourceLocation::new(source_path.clone(), 0));
94 }
95 all_words.push(word);
96 }
97
98 for mut union_def in program.unions {
99 if let Some(ref mut source) = union_def.source {
101 source.file = source_path.clone();
102 } else {
103 union_def.source = Some(SourceLocation::new(source_path.clone(), 0));
104 }
105 all_unions.push(union_def);
106 }
107
108 for include in &program.includes {
110 let content = self.process_include(include, source_dir)?;
111 all_words.extend(content.words);
112 all_unions.extend(content.unions);
113 }
114
115 let resolved_program = Program {
116 includes: Vec::new(), unions: all_unions,
118 words: all_words,
119 };
120
121 Ok(ResolveResult {
125 program: resolved_program,
126 ffi_includes: std::mem::take(&mut self.ffi_includes),
127 source_files: self.included_files.iter().cloned().collect(),
128 embedded_modules: self.included_embedded.iter().cloned().collect(),
129 })
130 }
131
132 fn process_include(
134 &mut self,
135 include: &Include,
136 source_dir: &Path,
137 ) -> Result<ResolvedContent, String> {
138 if let Include::Ffi(name) = include {
141 if !crate::ffi::has_ffi_manifest(name) {
143 return Err(format!(
144 "FFI library '{}' not found. Available: {}",
145 name,
146 crate::ffi::list_ffi_manifests().join(", ")
147 ));
148 }
149 if !self.ffi_includes.contains(name) {
151 self.ffi_includes.push(name.clone());
152 }
153 return Ok(ResolvedContent {
155 words: Vec::new(),
156 unions: Vec::new(),
157 });
158 }
159
160 let resolved = self.resolve_include(include, source_dir)?;
161
162 match resolved {
163 ResolvedInclude::Embedded(name, content) => {
164 self.process_embedded_include(&name, content, source_dir)
165 }
166 ResolvedInclude::FilePath(path) => self.process_file_include(&path),
167 }
168 }
169
170 fn process_embedded_include(
172 &mut self,
173 name: &str,
174 content: &str,
175 source_dir: &Path,
176 ) -> Result<ResolvedContent, String> {
177 if self.included_embedded.contains(name) {
179 return Ok(ResolvedContent {
180 words: Vec::new(),
181 unions: Vec::new(),
182 });
183 }
184 self.included_embedded.insert(name.to_string());
185
186 let mut parser = Parser::new(content);
188 let included_program = parser
189 .parse()
190 .map_err(|e| format!("Failed to parse embedded module '{}': {}", name, e))?;
191
192 let pseudo_path = PathBuf::from(format!("<stdlib:{}>", name));
194
195 let mut all_words = Vec::new();
197 for mut word in included_program.words {
198 if let Some(ref mut source) = word.source {
199 source.file = pseudo_path.clone();
200 } else {
201 word.source = Some(SourceLocation::new(pseudo_path.clone(), 0));
202 }
203 all_words.push(word);
204 }
205
206 let mut all_unions = Vec::new();
208 for mut union_def in included_program.unions {
209 if let Some(ref mut source) = union_def.source {
210 source.file = pseudo_path.clone();
211 } else {
212 union_def.source = Some(SourceLocation::new(pseudo_path.clone(), 0));
213 }
214 all_unions.push(union_def);
215 }
216
217 for include in &included_program.includes {
219 let content = self.process_include(include, source_dir)?;
220 all_words.extend(content.words);
221 all_unions.extend(content.unions);
222 }
223
224 Ok(ResolvedContent {
225 words: all_words,
226 unions: all_unions,
227 })
228 }
229
230 fn process_file_include(&mut self, path: &Path) -> Result<ResolvedContent, String> {
232 let canonical = path
234 .canonicalize()
235 .map_err(|e| format!("Failed to canonicalize {}: {}", path.display(), e))?;
236
237 if self.included_files.contains(&canonical) {
238 return Ok(ResolvedContent {
239 words: Vec::new(),
240 unions: Vec::new(),
241 });
242 }
243
244 let content = std::fs::read_to_string(path)
246 .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
247
248 let mut parser = Parser::new(&content);
249 let included_program = parser.parse()?;
250
251 let resolved = self.resolve(path, included_program)?;
253
254 Ok(ResolvedContent {
255 words: resolved.program.words,
256 unions: resolved.program.unions,
257 })
258 }
259
260 fn resolve_include(
262 &self,
263 include: &Include,
264 source_dir: &Path,
265 ) -> Result<ResolvedInclude, String> {
266 match include {
267 Include::Std(name) => {
268 if let Some(content) = stdlib_embed::get_stdlib(name) {
270 return Ok(ResolvedInclude::Embedded(name.clone(), content));
271 }
272
273 if let Some(ref stdlib_path) = self.stdlib_path {
275 let path = stdlib_path.join(format!("{}.seq", name));
276 if path.exists() {
277 return Ok(ResolvedInclude::FilePath(path));
278 }
279 }
280
281 Err(format!(
283 "Standard library module '{}' not found (not embedded{})",
284 name,
285 if self.stdlib_path.is_some() {
286 " and not in stdlib directory"
287 } else {
288 ""
289 }
290 ))
291 }
292 Include::Relative(rel_path) => Ok(ResolvedInclude::FilePath(
293 self.resolve_relative_path(rel_path, source_dir)?,
294 )),
295 Include::Ffi(_) => {
296 unreachable!("FFI includes should be handled before resolve_include is called")
298 }
299 }
300 }
301
302 fn resolve_relative_path(&self, rel_path: &str, source_dir: &Path) -> Result<PathBuf, String> {
307 if rel_path.is_empty() {
309 return Err("Include path cannot be empty".to_string());
310 }
311
312 let rel_as_path = std::path::Path::new(rel_path);
314 if rel_as_path.is_absolute() {
315 return Err(format!(
316 "Include path '{}' is invalid: paths cannot be absolute",
317 rel_path
318 ));
319 }
320
321 let path = source_dir.join(format!("{}.seq", rel_path));
322 if !path.exists() {
323 return Err(format!(
324 "Include file '{}' not found at {}",
325 rel_path,
326 path.display()
327 ));
328 }
329
330 let canonical_path = path
332 .canonicalize()
333 .map_err(|e| format!("Failed to resolve include path '{}': {}", rel_path, e))?;
334
335 Ok(canonical_path)
336 }
337}
338
339mod helpers;
342
343#[cfg(test)]
344mod tests;
345
346pub use helpers::{check_collisions, check_union_collisions, find_stdlib};