Skip to main content

sbpf_assembler/preprocessor/
mod.rs

1pub mod expand;
2pub mod include;
3pub mod macro_def;
4pub mod source_map;
5
6use {
7    crate::errors::CompileError,
8    source_map::{FileRegistry, SourceMap, SourceOrigin},
9    std::path::PathBuf,
10};
11
12/// A line of source with its origin tracking
13#[derive(Debug, Clone)]
14pub(crate) struct SourceLine {
15    pub text: String,
16    pub origin: SourceOrigin,
17}
18
19/// Trait for resolving `.include` file paths to their contents.
20/// Abstracted as a trait so tests can use an in-memory mock.
21pub trait FileResolver {
22    /// Resolve an include path relative to the including file's directory.
23    /// Returns the file contents.
24    fn resolve(&self, path: &str, relative_to: &str) -> Result<String, std::io::Error>;
25}
26
27/// File resolver that reads from the real filesystem.
28#[derive(Debug, Clone)]
29pub struct FsFileResolver {
30    /// Additional directories to search for includes
31    pub include_paths: Vec<PathBuf>,
32}
33
34impl FsFileResolver {
35    pub fn new() -> Self {
36        Self {
37            include_paths: Vec::new(),
38        }
39    }
40
41    pub fn with_include_paths(include_paths: Vec<PathBuf>) -> Self {
42        Self { include_paths }
43    }
44}
45
46impl Default for FsFileResolver {
47    fn default() -> Self {
48        Self::new()
49    }
50}
51
52impl FileResolver for FsFileResolver {
53    fn resolve(&self, path: &str, relative_to: &str) -> Result<String, std::io::Error> {
54        // Try relative to the including file's directory first
55        let base_dir = std::path::Path::new(relative_to)
56            .parent()
57            .unwrap_or(std::path::Path::new("."));
58        let candidate = base_dir.join(path);
59        if candidate.exists() {
60            return std::fs::read_to_string(&candidate);
61        }
62
63        // Try each include path
64        for include_dir in &self.include_paths {
65            let candidate = include_dir.join(path);
66            if candidate.exists() {
67                return std::fs::read_to_string(&candidate);
68            }
69        }
70
71        Err(std::io::Error::new(
72            std::io::ErrorKind::NotFound,
73            format!("file not found: {}", path),
74        ))
75    }
76}
77
78/// In-memory file resolver for testing
79#[derive(Debug, Clone, Default)]
80pub struct MockFileResolver {
81    files: std::collections::HashMap<String, String>,
82}
83
84impl MockFileResolver {
85    pub fn new() -> Self {
86        Self::default()
87    }
88
89    pub fn add_file(&mut self, path: &str, content: &str) -> &mut Self {
90        self.files.insert(path.to_string(), content.to_string());
91        self
92    }
93}
94
95impl FileResolver for MockFileResolver {
96    fn resolve(&self, path: &str, _relative_to: &str) -> Result<String, std::io::Error> {
97        self.files.get(path).cloned().ok_or_else(|| {
98            std::io::Error::new(
99                std::io::ErrorKind::NotFound,
100                format!("file not found: {}", path),
101            )
102        })
103    }
104}
105
106/// Result of preprocessing: expanded source text + a source map for diagnostics
107pub struct PreprocessResult {
108    pub expanded_source: String,
109    pub source_map: SourceMap,
110}
111
112/// A preprocessor error paired with its source origin.
113#[derive(Debug)]
114pub struct PreprocessorError {
115    pub error: CompileError,
116    pub origin: Option<SourceOrigin>,
117}
118
119/// Returned on preprocessing failure: errors + the file registry
120/// (so the caller can still render diagnostics against original files).
121pub struct PreprocessFailure {
122    pub errors: Vec<PreprocessorError>,
123    pub file_registry: FileRegistry,
124}
125
126/// Run the full preprocessor pipeline:
127/// 1. Resolve `.include` directives (flatten files)
128/// 2. Expand `.macro`/`.endm`, `.rept`/`.endr`, `.irp`/`.endr`
129///
130/// The resulting `expanded_source` can be fed directly to the pest parser.
131/// The `source_map` allows remapping pest error spans back to original locations.
132pub fn preprocess(
133    source: &str,
134    source_path: &str,
135    resolver: Option<&dyn FileResolver>,
136) -> Result<PreprocessResult, PreprocessFailure> {
137    let mut registry = FileRegistry::new();
138
139    // Pass 1: Include resolution
140    let lines = match include::resolve_includes(source, source_path, resolver, &mut registry) {
141        Ok(lines) => lines,
142        Err(errors) => {
143            return Err(PreprocessFailure {
144                errors: errors
145                    .into_iter()
146                    .map(|e| PreprocessorError {
147                        error: e,
148                        origin: None,
149                    })
150                    .collect(),
151                file_registry: registry,
152            });
153        }
154    };
155
156    // Pass 2: Macro expansion
157    let (expanded_lines, errors) = match expand::expand_macros(lines) {
158        Ok(result) => result,
159        Err(errors) => {
160            return Err(PreprocessFailure {
161                errors: errors
162                    .into_iter()
163                    .map(|e| PreprocessorError {
164                        error: e.error,
165                        origin: e.origin,
166                    })
167                    .collect(),
168                file_registry: registry,
169            });
170        }
171    };
172
173    if !errors.is_empty() {
174        return Err(PreprocessFailure {
175            errors: errors
176                .into_iter()
177                .map(|e| PreprocessorError {
178                    error: e.error,
179                    origin: e.origin,
180                })
181                .collect(),
182            file_registry: registry,
183        });
184    }
185
186    // Build the expanded source string and source map
187    let mut expanded_source = String::new();
188    let mut line_origins = Vec::with_capacity(expanded_lines.len());
189
190    for line in &expanded_lines {
191        expanded_source.push_str(&line.text);
192        expanded_source.push('\n');
193        line_origins.push(line.origin.clone());
194    }
195
196    let source_map = SourceMap::new(registry, line_origins);
197
198    Ok(PreprocessResult {
199        expanded_source,
200        source_map,
201    })
202}