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
35impl ResolvedContent {
36 fn empty() -> Self {
38 ResolvedContent {
39 words: Vec::new(),
40 unions: Vec::new(),
41 }
42 }
43}
44
45#[derive(Debug)]
47enum ResolvedInclude {
48 Embedded(String, &'static str),
50 FilePath(PathBuf),
52}
53
54pub struct Resolver {
56 included_files: HashSet<PathBuf>,
58 included_embedded: HashSet<String>,
60 stdlib_path: Option<PathBuf>,
62 ffi_includes: Vec<String>,
64}
65
66impl Resolver {
67 pub fn new(stdlib_path: Option<PathBuf>) -> Self {
69 Resolver {
70 included_files: HashSet::new(),
71 included_embedded: HashSet::new(),
72 stdlib_path,
73 ffi_includes: Vec::new(),
74 }
75 }
76
77 pub fn resolve(
83 &mut self,
84 source_path: &Path,
85 program: Program,
86 ) -> Result<ResolveResult, String> {
87 let source_path = source_path
88 .canonicalize()
89 .map_err(|e| format!("Failed to canonicalize {}: {}", source_path.display(), e))?;
90
91 self.included_files.insert(source_path.clone());
93
94 let source_dir = source_path.parent().unwrap_or(Path::new("."));
95 let mut all_words = Vec::new();
96 let mut all_unions = Vec::new();
97
98 for mut word in program.words {
99 stamp_source(&mut word.source, &source_path);
100 all_words.push(word);
101 }
102
103 for mut union_def in program.unions {
104 stamp_source(&mut union_def.source, &source_path);
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::empty());
155 }
156
157 let resolved = self.resolve_include(include, source_dir)?;
158
159 match resolved {
160 ResolvedInclude::Embedded(name, content) => {
161 self.process_embedded_include(&name, content, source_dir)
162 }
163 ResolvedInclude::FilePath(path) => self.process_file_include(&path),
164 }
165 }
166
167 fn process_embedded_include(
169 &mut self,
170 name: &str,
171 content: &str,
172 source_dir: &Path,
173 ) -> Result<ResolvedContent, String> {
174 if self.included_embedded.contains(name) {
176 return Ok(ResolvedContent::empty());
177 }
178 self.included_embedded.insert(name.to_string());
179
180 let mut parser = Parser::new(content);
182 let included_program = parser
183 .parse()
184 .map_err(|e| format!("Failed to parse embedded module '{}': {}", name, e))?;
185
186 let pseudo_path = PathBuf::from(format!("<stdlib:{}>", name));
188
189 let mut all_words = Vec::new();
191 for mut word in included_program.words {
192 stamp_source(&mut word.source, &pseudo_path);
193 all_words.push(word);
194 }
195
196 let mut all_unions = Vec::new();
197 for mut union_def in included_program.unions {
198 stamp_source(&mut union_def.source, &pseudo_path);
199 all_unions.push(union_def);
200 }
201
202 for include in &included_program.includes {
204 let content = self.process_include(include, source_dir)?;
205 all_words.extend(content.words);
206 all_unions.extend(content.unions);
207 }
208
209 Ok(ResolvedContent {
210 words: all_words,
211 unions: all_unions,
212 })
213 }
214
215 fn process_file_include(&mut self, path: &Path) -> Result<ResolvedContent, String> {
217 let canonical = path
219 .canonicalize()
220 .map_err(|e| format!("Failed to canonicalize {}: {}", path.display(), e))?;
221
222 if self.included_files.contains(&canonical) {
223 return Ok(ResolvedContent::empty());
224 }
225
226 let content = std::fs::read_to_string(path)
228 .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
229
230 let mut parser = Parser::new(&content);
231 let included_program = parser.parse()?;
232
233 let resolved = self.resolve(path, included_program)?;
235
236 Ok(ResolvedContent {
237 words: resolved.program.words,
238 unions: resolved.program.unions,
239 })
240 }
241
242 fn resolve_include(
244 &self,
245 include: &Include,
246 source_dir: &Path,
247 ) -> Result<ResolvedInclude, String> {
248 match include {
249 Include::Std(name) => {
250 if let Some(content) = stdlib_embed::get_stdlib(name) {
252 return Ok(ResolvedInclude::Embedded(name.clone(), content));
253 }
254
255 if let Some(ref stdlib_path) = self.stdlib_path {
257 let path = stdlib_path.join(format!("{}.seq", name));
258 if path.exists() {
259 return Ok(ResolvedInclude::FilePath(path));
260 }
261 }
262
263 Err(format!(
265 "Standard library module '{}' not found (not embedded{})",
266 name,
267 if self.stdlib_path.is_some() {
268 " and not in stdlib directory"
269 } else {
270 ""
271 }
272 ))
273 }
274 Include::Relative(rel_path) => Ok(ResolvedInclude::FilePath(
275 self.resolve_relative_path(rel_path, source_dir)?,
276 )),
277 Include::Ffi(_) => {
278 unreachable!("FFI includes should be handled before resolve_include is called")
280 }
281 }
282 }
283
284 fn resolve_relative_path(&self, rel_path: &str, source_dir: &Path) -> Result<PathBuf, String> {
289 if rel_path.is_empty() {
291 return Err("Include path cannot be empty".to_string());
292 }
293
294 let rel_as_path = std::path::Path::new(rel_path);
296 if rel_as_path.is_absolute() {
297 return Err(format!(
298 "Include path '{}' is invalid: paths cannot be absolute",
299 rel_path
300 ));
301 }
302
303 let path = source_dir.join(format!("{}.seq", rel_path));
304 if !path.exists() {
305 return Err(format!(
306 "Include file '{}' not found at {}",
307 rel_path,
308 path.display()
309 ));
310 }
311
312 let canonical_path = path
314 .canonicalize()
315 .map_err(|e| format!("Failed to resolve include path '{}': {}", rel_path, e))?;
316
317 Ok(canonical_path)
318 }
319}
320
321fn stamp_source(source: &mut Option<SourceLocation>, file: &Path) {
324 match source {
325 Some(loc) => loc.file = file.to_path_buf(),
326 None => *source = Some(SourceLocation::new(file.to_path_buf(), 0)),
327 }
328}
329
330mod helpers;
331
332#[cfg(test)]
333mod tests;
334
335pub use helpers::{check_collisions, check_union_collisions, find_stdlib};