Skip to main content

seqc/
resolver.rs

1//! Include Resolver for Seq
2//!
3//! Resolves include statements, loads and parses included files,
4//! and merges everything into a single Program.
5//!
6//! Supports:
7//! - `include std:name` - loads from embedded stdlib (or filesystem fallback)
8//! - `include ffi:name` - loads FFI manifest (collected but not processed here)
9//! - `include "path"` - loads relative to current file
10
11use 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
17/// Result of resolving includes
18pub struct ResolveResult {
19    /// The resolved program with all includes merged
20    pub program: Program,
21    /// FFI library names that were included (e.g., ["readline"])
22    pub ffi_includes: Vec<String>,
23    /// Filesystem .seq files that were included (for cache invalidation)
24    pub source_files: Vec<PathBuf>,
25    /// Embedded stdlib modules that were included
26    pub embedded_modules: Vec<String>,
27}
28
29/// Words and unions collected from a resolved include
30struct ResolvedContent {
31    words: Vec<WordDef>,
32    unions: Vec<UnionDef>,
33}
34
35impl ResolvedContent {
36    /// An include that contributed no words or unions.
37    fn empty() -> Self {
38        ResolvedContent {
39            words: Vec::new(),
40            unions: Vec::new(),
41        }
42    }
43}
44
45/// Result of resolving an include - either embedded content or a file path
46#[derive(Debug)]
47enum ResolvedInclude {
48    /// Embedded stdlib content (name, content)
49    Embedded(String, &'static str),
50    /// File system path
51    FilePath(PathBuf),
52}
53
54/// Resolver for include statements
55pub struct Resolver {
56    /// Set of files already included (canonical paths to prevent double-include)
57    included_files: HashSet<PathBuf>,
58    /// Set of embedded stdlib modules already included
59    included_embedded: HashSet<String>,
60    /// Path to stdlib directory (fallback for non-embedded modules), if available
61    stdlib_path: Option<PathBuf>,
62    /// FFI libraries that were included
63    ffi_includes: Vec<String>,
64}
65
66impl Resolver {
67    /// Create a new resolver with an optional stdlib path for filesystem fallback
68    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    /// Resolve all includes in a program and return a merged program with FFI includes
78    ///
79    /// Takes the source file path and its already-parsed program.
80    /// Recursively resolves includes and merges all word and union definitions.
81    /// FFI includes are collected but not processed (they don't produce words/unions).
82    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        // Mark this file as included
92        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        // Process includes
109        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(), // Includes are resolved, no longer needed
117            unions: all_unions,
118            words: all_words,
119        };
120
121        // Note: Constructor generation is done in lib.rs after resolution
122        // to keep all constructor generation in one place
123
124        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    /// Process a single include and return the resolved words and unions
133    fn process_include(
134        &mut self,
135        include: &Include,
136        source_dir: &Path,
137    ) -> Result<ResolvedContent, String> {
138        // Handle FFI includes specially - they don't produce words/unions,
139        // they're collected for later processing by the FFI system
140        if let Include::Ffi(name) = include {
141            // Check if we have the FFI manifest
142            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            // Avoid duplicate FFI includes
150            if !self.ffi_includes.contains(name) {
151                self.ffi_includes.push(name.clone());
152            }
153            // FFI includes don't add words/unions directly
154            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    /// Process an embedded stdlib include
168    fn process_embedded_include(
169        &mut self,
170        name: &str,
171        content: &str,
172        source_dir: &Path,
173    ) -> Result<ResolvedContent, String> {
174        // Skip if already included
175        if self.included_embedded.contains(name) {
176            return Ok(ResolvedContent::empty());
177        }
178        self.included_embedded.insert(name.to_string());
179
180        // Parse the embedded content
181        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        // Create a pseudo-path for source locations
187        let pseudo_path = PathBuf::from(format!("<stdlib:{}>", name));
188
189        // Collect words and unions with updated source locations
190        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        // Recursively process includes from embedded module
203        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    /// Process a filesystem include
216    fn process_file_include(&mut self, path: &Path) -> Result<ResolvedContent, String> {
217        // Skip if already included (prevents diamond dependency issues)
218        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        // Read and parse the included file
227        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        // Recursively resolve includes in the included file
234        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    /// Resolve an include to either embedded content or a file path
243    fn resolve_include(
244        &self,
245        include: &Include,
246        source_dir: &Path,
247    ) -> Result<ResolvedInclude, String> {
248        match include {
249            Include::Std(name) => {
250                // Check embedded stdlib first
251                if let Some(content) = stdlib_embed::get_stdlib(name) {
252                    return Ok(ResolvedInclude::Embedded(name.clone(), content));
253                }
254
255                // Fall back to filesystem if stdlib_path is available
256                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                // Not found anywhere
264                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                // FFI includes are handled separately in process_include
279                unreachable!("FFI includes should be handled before resolve_include is called")
280            }
281        }
282    }
283
284    /// Resolve a relative include path to a file path
285    ///
286    /// Paths can contain `..` to reference parent directories, but the resolved
287    /// path must stay within the project root (main source file's directory).
288    fn resolve_relative_path(&self, rel_path: &str, source_dir: &Path) -> Result<PathBuf, String> {
289        // Validate non-empty path
290        if rel_path.is_empty() {
291            return Err("Include path cannot be empty".to_string());
292        }
293
294        // Cross-platform absolute path detection
295        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        // Canonicalize to resolve symlinks and normalize the path
313        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
321/// Stamp `file` onto a definition's source location, creating one at line 0
322/// if the definition has none yet.
323fn 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};