Skip to main content

snc_core/
preprocess.rs

1/// Simple C preprocessor for SNL files.
2///
3/// Supports object-like `#define NAME value` macros (integer, float, string literals).
4/// Function-like macros are not supported and produce a warning.
5/// `#include` is skipped with a warning.
6/// Line mapping is maintained so Span refers to original source lines.
7
8use std::collections::HashMap;
9
10/// Result of preprocessing: transformed source + line mapping.
11pub struct PreprocessResult {
12    /// The preprocessed source text.
13    pub source: String,
14    /// Maps output line number (0-based) → original line number (0-based).
15    pub line_map: Vec<usize>,
16    /// Any warnings generated during preprocessing.
17    pub warnings: Vec<String>,
18}
19
20/// Preprocess an SNL source string, expanding object-like #define macros.
21pub 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                            // #define NAME with no value — define as empty string
49                            defines.insert(name, String::new());
50                        }
51                    }
52                }
53                // Emit empty line to preserve line count
54                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                // Skip conditional compilation directives
77                output_lines.push(String::new());
78                line_map.push(line_no);
79            } else {
80                // Unknown directive, pass through as empty
81                output_lines.push(String::new());
82                line_map.push(line_no);
83            }
84        } else {
85            // Substitute macros in this line
86            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    // Read macro name
109    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    // Check for function-like macro: name immediately followed by '('
124    if chars.peek() == Some(&'(') {
125        return Some(DefineResult::FunctionLike { name });
126    }
127
128    // Skip whitespace after name
129    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    // Strip any trailing comments
137    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
152/// Substitute all known macros in a line.
153/// Uses word-boundary matching to avoid replacing inside identifiers.
154fn 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        // Replace whole-word occurrences only
165        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                // Check word boundaries
174                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        // SQR should NOT be expanded in the source
226        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        // Line 0 = #define (empty), Line 1 = int x = 5, Line 2 = int y = 5
250        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        // NMAX should NOT be replaced
261        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        // FEATURE with empty value — should replace with nothing
271        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        // #ifdef and #endif should be empty lines, but content between is passed through
286        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}