Skip to main content

sbpf_assembler/preprocessor/
include.rs

1use {
2    super::{
3        FileResolver, SourceLine,
4        source_map::{FileRegistry, SourceOrigin},
5    },
6    crate::errors::CompileError,
7    std::collections::HashSet,
8};
9
10/// Parse a `.include "path"` directive from a line.
11/// Returns Some(path) if the line is an include directive, None otherwise.
12fn parse_include_directive(line: &str) -> Option<&str> {
13    let trimmed = line.trim();
14    let rest = trimmed.strip_prefix(".include")?;
15
16    // Must have whitespace after .include
17    if !rest.starts_with(char::is_whitespace) {
18        return None;
19    }
20
21    let rest = rest.trim();
22
23    // Path must be quoted
24    if rest.starts_with('"') && rest.ends_with('"') && rest.len() >= 2 {
25        Some(&rest[1..rest.len() - 1])
26    } else {
27        None
28    }
29}
30
31/// Resolve all `.include` directives recursively, producing a flat list of source lines.
32///
33/// Each line tracks its origin (file + line number) for diagnostics.
34/// Cycle detection uses a stack-based approach: A including B including A is caught,
35/// but A including B, then A including C (which also includes B) is allowed.
36pub(crate) fn resolve_includes(
37    source: &str,
38    source_path: &str,
39    resolver: Option<&dyn FileResolver>,
40    registry: &mut FileRegistry,
41) -> Result<Vec<SourceLine>, Vec<CompileError>> {
42    let mut include_stack: HashSet<String> = HashSet::new();
43    include_stack.insert(source_path.to_string());
44
45    let file_id = registry.add(source_path, source.to_string());
46    let mut errors = Vec::new();
47    let mut output = Vec::new();
48
49    resolve_recursive(
50        source,
51        file_id,
52        source_path,
53        resolver,
54        registry,
55        &mut include_stack,
56        &mut output,
57        &mut errors,
58    );
59
60    if errors.is_empty() {
61        Ok(output)
62    } else {
63        Err(errors)
64    }
65}
66
67#[allow(clippy::too_many_arguments)]
68fn resolve_recursive(
69    source: &str,
70    file_id: super::source_map::FileId,
71    file_path: &str,
72    resolver: Option<&dyn FileResolver>,
73    registry: &mut FileRegistry,
74    include_stack: &mut HashSet<String>,
75    output: &mut Vec<SourceLine>,
76    errors: &mut Vec<CompileError>,
77) {
78    for (line_idx, line_text) in source.lines().enumerate() {
79        let line_number = (line_idx + 1) as u32;
80
81        if let Some(include_path) = parse_include_directive(line_text) {
82            // Calculate a span for error reporting.
83            // We use the byte offset of this line in the source.
84            let line_start: usize = source
85                .lines()
86                .take(line_idx)
87                .map(|l| l.len() + 1) // +1 for newline
88                .sum();
89            let span = line_start..line_start + line_text.len();
90
91            let resolver = match resolver {
92                Some(r) => r,
93                None => {
94                    errors.push(CompileError::IncludeNotFound {
95                        path: include_path.to_string(),
96                        span,
97                        custom_label: Some("No file resolver configured".to_string()),
98                    });
99                    continue;
100                }
101            };
102
103            // Check for include cycle
104            if include_stack.contains(include_path) {
105                errors.push(CompileError::IncludeCycle {
106                    path: include_path.to_string(),
107                    span,
108                    custom_label: None,
109                });
110                continue;
111            }
112
113            // Resolve and read the file
114            match resolver.resolve(include_path, file_path) {
115                Ok(content) => {
116                    let included_file_id = registry.add(include_path, content.clone());
117                    include_stack.insert(include_path.to_string());
118
119                    resolve_recursive(
120                        &content,
121                        included_file_id,
122                        include_path,
123                        Some(resolver),
124                        registry,
125                        include_stack,
126                        output,
127                        errors,
128                    );
129
130                    include_stack.remove(include_path);
131                }
132                Err(e) => {
133                    if e.kind() == std::io::ErrorKind::NotFound {
134                        errors.push(CompileError::IncludeNotFound {
135                            path: include_path.to_string(),
136                            span,
137                            custom_label: None,
138                        });
139                    } else {
140                        errors.push(CompileError::IncludeReadError {
141                            path: include_path.to_string(),
142                            reason: e.to_string(),
143                            span,
144                            custom_label: None,
145                        });
146                    }
147                }
148            }
149        } else {
150            // Regular line -- pass through with origin tracking
151            output.push(SourceLine {
152                text: line_text.to_string(),
153                origin: SourceOrigin::new(file_id, line_number),
154            });
155        }
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use {super::*, crate::preprocessor::MockFileResolver};
162
163    #[test]
164    fn test_parse_include_directive() {
165        assert_eq!(parse_include_directive(".include \"foo.s\""), Some("foo.s"));
166        assert_eq!(
167            parse_include_directive("  .include \"path/to/file.s\"  "),
168            Some("path/to/file.s")
169        );
170        assert_eq!(parse_include_directive(".include foo.s"), None);
171        assert_eq!(parse_include_directive("mov64 r1, 1"), None);
172        assert_eq!(parse_include_directive(".includes \"foo.s\""), None);
173        assert_eq!(parse_include_directive(".include"), None);
174    }
175
176    #[test]
177    fn test_no_includes() {
178        let source = "mov64 r1, 1\nexit\n";
179        let mut registry = FileRegistry::new();
180        let result = resolve_includes(source, "<input>", None, &mut registry).unwrap();
181
182        assert_eq!(result.len(), 2);
183        assert_eq!(result[0].text, "mov64 r1, 1");
184        assert_eq!(result[0].origin.line, 1);
185        assert_eq!(result[1].text, "exit");
186        assert_eq!(result[1].origin.line, 2);
187    }
188
189    #[test]
190    fn test_single_include() {
191        let mut resolver = MockFileResolver::new();
192        resolver.add_file("macros.s", "macro_line1\nmacro_line2");
193
194        let source = "before\n.include \"macros.s\"\nafter";
195        let mut registry = FileRegistry::new();
196        let result = resolve_includes(source, "<input>", Some(&resolver), &mut registry).unwrap();
197
198        assert_eq!(result.len(), 4);
199        assert_eq!(result[0].text, "before");
200        assert_eq!(result[1].text, "macro_line1");
201        assert_eq!(result[2].text, "macro_line2");
202        assert_eq!(result[3].text, "after");
203
204        // Check origin tracking
205        assert_eq!(registry.path(result[0].origin.file_id), "<input>");
206        assert_eq!(registry.path(result[1].origin.file_id), "macros.s");
207        assert_eq!(result[1].origin.line, 1);
208        assert_eq!(result[2].origin.line, 2);
209    }
210
211    #[test]
212    fn test_nested_includes() {
213        let mut resolver = MockFileResolver::new();
214        resolver.add_file("a.s", "a_line\n.include \"b.s\"\na_end");
215        resolver.add_file("b.s", "b_line");
216
217        let source = ".include \"a.s\"";
218        let mut registry = FileRegistry::new();
219        let result = resolve_includes(source, "<input>", Some(&resolver), &mut registry).unwrap();
220
221        assert_eq!(result.len(), 3);
222        assert_eq!(result[0].text, "a_line");
223        assert_eq!(result[1].text, "b_line");
224        assert_eq!(result[2].text, "a_end");
225    }
226
227    #[test]
228    fn test_include_cycle_detection() {
229        let mut resolver = MockFileResolver::new();
230        resolver.add_file("a.s", ".include \"<input>\"");
231
232        let source = ".include \"a.s\"";
233        let mut registry = FileRegistry::new();
234        let result = resolve_includes(source, "<input>", Some(&resolver), &mut registry);
235
236        assert!(result.is_err());
237        let errors = result.unwrap_err();
238        assert_eq!(errors.len(), 1);
239        assert!(matches!(&errors[0], CompileError::IncludeCycle { path, .. } if path == "<input>"));
240    }
241
242    #[test]
243    fn test_include_not_found() {
244        let resolver = MockFileResolver::new();
245
246        let source = ".include \"missing.s\"";
247        let mut registry = FileRegistry::new();
248        let result = resolve_includes(source, "<input>", Some(&resolver), &mut registry);
249
250        assert!(result.is_err());
251        let errors = result.unwrap_err();
252        assert!(
253            matches!(&errors[0], CompileError::IncludeNotFound { path, .. } if path == "missing.s")
254        );
255    }
256
257    #[test]
258    fn test_diamond_include_allowed() {
259        // A includes B and C, both B and C include D -- this is allowed (not a cycle)
260        let mut resolver = MockFileResolver::new();
261        resolver.add_file("b.s", ".include \"d.s\"\nb_line");
262        resolver.add_file("c.s", ".include \"d.s\"\nc_line");
263        resolver.add_file("d.s", "d_line");
264
265        let source = ".include \"b.s\"\n.include \"c.s\"";
266        let mut registry = FileRegistry::new();
267        let result = resolve_includes(source, "<input>", Some(&resolver), &mut registry).unwrap();
268
269        assert_eq!(result.len(), 4);
270        assert_eq!(result[0].text, "d_line");
271        assert_eq!(result[1].text, "b_line");
272        assert_eq!(result[2].text, "d_line");
273        assert_eq!(result[3].text, "c_line");
274    }
275}