1use std::collections::HashMap;
9
10pub struct PreprocessResult {
12 pub source: String,
14 pub line_map: Vec<usize>,
16 pub warnings: Vec<String>,
18}
19
20pub fn preprocess(input: &str) -> PreprocessResult {
22 let mut defines: HashMap<String, String> = HashMap::new();
23 let mut output_lines = Vec::new();
24 let mut line_map = Vec::new();
25 let mut warnings = Vec::new();
26
27 for (line_no, line) in input.lines().enumerate() {
28 let trimmed = line.trim_start();
29
30 if trimmed.starts_with('#') {
31 let directive = trimmed.trim_start_matches('#').trim_start();
32
33 if directive.starts_with("define") {
34 let rest = directive["define".len()..].trim_start();
35 if let Some(result) = parse_define(rest) {
36 match result {
37 DefineResult::ObjectLike { name, value } => {
38 defines.insert(name, value);
39 }
40 DefineResult::FunctionLike { name } => {
41 warnings.push(format!(
42 "line {}: function-like macro '{}' not supported, skipping",
43 line_no + 1,
44 name
45 ));
46 }
47 DefineResult::Empty { name } => {
48 defines.insert(name, String::new());
50 }
51 }
52 }
53 output_lines.push(String::new());
55 line_map.push(line_no);
56 } else if directive.starts_with("include") {
57 warnings.push(format!(
58 "line {}: #include not supported, skipping",
59 line_no + 1
60 ));
61 output_lines.push(String::new());
62 line_map.push(line_no);
63 } else if directive.starts_with("undef") {
64 let rest = directive["undef".len()..].trim_start();
65 let name = rest.split_whitespace().next().unwrap_or("").to_string();
66 defines.remove(&name);
67 output_lines.push(String::new());
68 line_map.push(line_no);
69 } else if directive.starts_with("ifdef")
70 || directive.starts_with("ifndef")
71 || directive.starts_with("if ")
72 || directive.starts_with("elif")
73 || directive.starts_with("else")
74 || directive.starts_with("endif")
75 {
76 output_lines.push(String::new());
78 line_map.push(line_no);
79 } else {
80 output_lines.push(String::new());
82 line_map.push(line_no);
83 }
84 } else {
85 let substituted = substitute_macros(line, &defines);
87 output_lines.push(substituted);
88 line_map.push(line_no);
89 }
90 }
91
92 PreprocessResult {
93 source: output_lines.join("\n"),
94 line_map,
95 warnings,
96 }
97}
98
99enum DefineResult {
100 ObjectLike { name: String, value: String },
101 FunctionLike { name: String },
102 Empty { name: String },
103}
104
105fn parse_define(rest: &str) -> Option<DefineResult> {
106 let mut chars = rest.chars().peekable();
107
108 let mut name = String::new();
110 while let Some(&ch) = chars.peek() {
111 if ch.is_ascii_alphanumeric() || ch == '_' {
112 name.push(ch);
113 chars.next();
114 } else {
115 break;
116 }
117 }
118
119 if name.is_empty() {
120 return None;
121 }
122
123 if chars.peek() == Some(&'(') {
125 return Some(DefineResult::FunctionLike { name });
126 }
127
128 while chars.peek().map_or(false, |c| c.is_ascii_whitespace()) {
130 chars.next();
131 }
132
133 let value: String = chars.collect();
134 let value = value.trim().to_string();
135
136 let value = if let Some(idx) = value.find("//") {
138 value[..idx].trim().to_string()
139 } else if let Some(idx) = value.find("/*") {
140 value[..idx].trim().to_string()
141 } else {
142 value
143 };
144
145 if value.is_empty() {
146 Some(DefineResult::Empty { name })
147 } else {
148 Some(DefineResult::ObjectLike { name, value })
149 }
150}
151
152fn substitute_macros(line: &str, defines: &HashMap<String, String>) -> String {
155 if defines.is_empty() {
156 return line.to_string();
157 }
158
159 let mut result = line.to_string();
160 for (name, value) in defines {
161 if !result.contains(name.as_str()) {
162 continue;
163 }
164 let mut new_result = String::with_capacity(result.len());
166 let bytes = result.as_bytes();
167 let name_bytes = name.as_bytes();
168 let mut i = 0;
169 while i < bytes.len() {
170 if i + name_bytes.len() <= bytes.len()
171 && &bytes[i..i + name_bytes.len()] == name_bytes
172 {
173 let before_ok = i == 0
175 || !(bytes[i - 1].is_ascii_alphanumeric() || bytes[i - 1] == b'_');
176 let after_ok = i + name_bytes.len() >= bytes.len()
177 || !(bytes[i + name_bytes.len()].is_ascii_alphanumeric()
178 || bytes[i + name_bytes.len()] == b'_');
179 if before_ok && after_ok {
180 new_result.push_str(value);
181 i += name_bytes.len();
182 continue;
183 }
184 }
185 new_result.push(bytes[i] as char);
186 i += 1;
187 }
188 result = new_result;
189 }
190 result
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn test_basic_define() {
199 let input = "#define MAX 100\nint x = MAX;\n";
200 let result = preprocess(input);
201 assert!(result.source.contains("int x = 100;"));
202 assert!(result.warnings.is_empty());
203 }
204
205 #[test]
206 fn test_float_define() {
207 let input = "#define PI 3.14159\ndouble x = PI;\n";
208 let result = preprocess(input);
209 assert!(result.source.contains("double x = 3.14159;"));
210 }
211
212 #[test]
213 fn test_string_define() {
214 let input = "#define PV_NAME \"MOTOR:POS\"\nassign x to PV_NAME;\n";
215 let result = preprocess(input);
216 assert!(result.source.contains("assign x to \"MOTOR:POS\";"));
217 }
218
219 #[test]
220 fn test_function_like_macro_warning() {
221 let input = "#define SQR(x) ((x)*(x))\nint y = SQR(3);\n";
222 let result = preprocess(input);
223 assert_eq!(result.warnings.len(), 1);
224 assert!(result.warnings[0].contains("function-like"));
225 assert!(result.source.contains("SQR(3)"));
227 }
228
229 #[test]
230 fn test_include_warning() {
231 let input = "#include \"foo.h\"\nint x;\n";
232 let result = preprocess(input);
233 assert_eq!(result.warnings.len(), 1);
234 assert!(result.warnings[0].contains("#include"));
235 }
236
237 #[test]
238 fn test_undef() {
239 let input = "#define X 10\nint a = X;\n#undef X\nint b = X;\n";
240 let result = preprocess(input);
241 assert!(result.source.contains("int a = 10;"));
242 assert!(result.source.contains("int b = X;"));
243 }
244
245 #[test]
246 fn test_line_mapping() {
247 let input = "#define N 5\nint x = N;\nint y = N;\n";
248 let result = preprocess(input);
249 assert_eq!(result.line_map.len(), 3);
251 assert_eq!(result.line_map[0], 0);
252 assert_eq!(result.line_map[1], 1);
253 assert_eq!(result.line_map[2], 2);
254 }
255
256 #[test]
257 fn test_word_boundary() {
258 let input = "#define N 5\nint NMAX = 10;\nint x = N;\n";
259 let result = preprocess(input);
260 assert!(result.source.contains("int NMAX = 10;"));
262 assert!(result.source.contains("int x = 5;"));
263 }
264
265 #[test]
266 fn test_empty_define() {
267 let input = "#define FEATURE\nint x;\n";
268 let result = preprocess(input);
269 assert!(result.warnings.is_empty());
270 assert!(result.source.contains("int x;"));
272 }
273
274 #[test]
275 fn test_define_with_comment() {
276 let input = "#define MAX 100 // maximum value\nint x = MAX;\n";
277 let result = preprocess(input);
278 assert!(result.source.contains("int x = 100;"));
279 }
280
281 #[test]
282 fn test_conditional_directives_skipped() {
283 let input = "#ifdef FOO\nint x;\n#endif\nint y;\n";
284 let result = preprocess(input);
285 assert!(result.source.contains("int x;"));
287 assert!(result.source.contains("int y;"));
288 }
289
290 #[test]
291 fn test_multiple_defines() {
292 let input = "#define A 1\n#define B 2\nint x = A + B;\n";
293 let result = preprocess(input);
294 assert!(result.source.contains("int x = 1 + 2;"));
295 }
296}