sbpf_assembler/preprocessor/
include.rs1use {
2 super::{
3 FileResolver, SourceLine,
4 source_map::{FileRegistry, SourceOrigin},
5 },
6 crate::errors::CompileError,
7 std::collections::HashSet,
8};
9
10fn parse_include_directive(line: &str) -> Option<&str> {
13 let trimmed = line.trim();
14 let rest = trimmed.strip_prefix(".include")?;
15
16 if !rest.starts_with(char::is_whitespace) {
18 return None;
19 }
20
21 let rest = rest.trim();
22
23 if rest.starts_with('"') && rest.ends_with('"') && rest.len() >= 2 {
25 Some(&rest[1..rest.len() - 1])
26 } else {
27 None
28 }
29}
30
31pub(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 let line_start: usize = source
85 .lines()
86 .take(line_idx)
87 .map(|l| l.len() + 1) .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 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 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 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 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 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}