Skip to main content

sbpf_assembler/preprocessor/
macro_def.rs

1use {
2    super::{SourceLine, source_map::SourceOrigin},
3    crate::errors::CompileError,
4};
5
6/// A macro parameter
7#[derive(Debug, Clone)]
8pub struct Param {
9    pub name: String,
10    pub default: Option<String>,
11    pub is_vararg: bool,
12}
13
14/// A parsed macro definition
15#[derive(Debug, Clone)]
16pub struct MacroDef {
17    pub name: String,
18    pub params: Vec<Param>,
19    pub body_lines: Vec<String>,
20    pub defined_at: SourceOrigin,
21}
22
23/// Result of scanning lines for macro definitions
24pub(crate) struct MacroScanResult {
25    /// All macro definitions found, keyed by name
26    pub macros: std::collections::HashMap<String, MacroDef>,
27    /// Lines that are not part of macro definitions (pass-through)
28    pub remaining_lines: Vec<SourceLine>,
29    /// Any errors encountered during scanning, paired with their source origin
30    pub errors: Vec<(CompileError, SourceOrigin)>,
31}
32
33/// Scan lines for `.macro` / `.endm` blocks, extracting definitions.
34///
35/// Lines between `.macro` and `.endm` become the macro body.
36/// All other lines are returned in `remaining_lines` for further processing.
37pub(crate) fn scan_macro_definitions(lines: Vec<SourceLine>) -> MacroScanResult {
38    let mut macros = std::collections::HashMap::new();
39    let mut remaining = Vec::new();
40    let mut errors = Vec::new();
41
42    let mut current_macro: Option<(String, Vec<Param>, SourceOrigin, Vec<String>)> = None;
43
44    // Track the origin of the .macro directive line for error reporting
45    let mut macro_start_origin: Option<SourceOrigin> = None;
46
47    for line in lines {
48        let trimmed = line.text.trim();
49
50        if let Some(ref mut macro_state) = current_macro {
51            if trimmed == ".endm" {
52                // Close the macro definition
53                let (name, params, origin, body) = current_macro.take().unwrap();
54                macro_start_origin = None;
55
56                if macros.contains_key(&name) {
57                    errors.push((
58                        CompileError::DuplicateMacroDef {
59                            name: name.clone(),
60                            span: 0..0,
61                            custom_label: None,
62                        },
63                        origin,
64                    ));
65                } else {
66                    macros.insert(
67                        name.clone(),
68                        MacroDef {
69                            name,
70                            params,
71                            body_lines: body,
72                            defined_at: origin,
73                        },
74                    );
75                }
76            } else {
77                // Accumulate body lines
78                macro_state.3.push(line.text.clone());
79            }
80        } else if let Some((name, params)) = parse_macro_directive(trimmed) {
81            // Validate parameters
82            let mut has_errors = false;
83            let mut seen_vararg = false;
84            for (i, param) in params.iter().enumerate() {
85                if param.is_vararg {
86                    if seen_vararg {
87                        errors.push((
88                            CompileError::MultipleVararg {
89                                name: name.clone(),
90                                span: 0..0,
91                                custom_label: None,
92                            },
93                            line.origin.clone(),
94                        ));
95                        has_errors = true;
96                        break;
97                    }
98                    if i != params.len() - 1 {
99                        errors.push((
100                            CompileError::VarargNotLast {
101                                name: name.clone(),
102                                span: 0..0,
103                                custom_label: None,
104                            },
105                            line.origin.clone(),
106                        ));
107                        has_errors = true;
108                        break;
109                    }
110                    seen_vararg = true;
111                }
112            }
113            if !has_errors {
114                macro_start_origin = Some(line.origin.clone());
115                current_macro = Some((name, params, line.origin.clone(), Vec::new()));
116            }
117        } else {
118            remaining.push(line);
119        }
120    }
121
122    // Check for unclosed macro at end of input
123    if let Some((name, _, _, _)) = current_macro {
124        let origin =
125            macro_start_origin.unwrap_or(SourceOrigin::new(super::source_map::FileId(0), 0));
126        errors.push((
127            CompileError::UnclosedMacro {
128                name,
129                span: 0..0,
130                custom_label: None,
131            },
132            origin,
133        ));
134    }
135
136    MacroScanResult {
137        macros,
138        remaining_lines: remaining,
139        errors,
140    }
141}
142
143/// Parse a `.macro name param1, param2=default, param3:vararg` directive.
144/// Returns the macro name and parsed parameters.
145fn parse_macro_directive(line: &str) -> Option<(String, Vec<Param>)> {
146    let rest = line.strip_prefix(".macro")?;
147
148    // Must have whitespace after .macro
149    if !rest.starts_with(char::is_whitespace) {
150        return None;
151    }
152
153    let rest = rest.trim();
154    if rest.is_empty() {
155        return None;
156    }
157
158    // First token is the macro name
159    let (name, params_str) = match rest.find(char::is_whitespace) {
160        Some(pos) => (&rest[..pos], rest[pos..].trim()),
161        None => (rest, ""),
162    };
163
164    let params = if params_str.is_empty() {
165        Vec::new()
166    } else {
167        parse_params(params_str)?
168    };
169
170    Some((name.to_string(), params))
171}
172
173/// Parse a comma-separated parameter list.
174/// Supports: `name`, `name=default`, `name:vararg`
175fn parse_params(params_str: &str) -> Option<Vec<Param>> {
176    let mut params = Vec::new();
177
178    for part in params_str.split(',') {
179        let part = part.trim();
180        if part.is_empty() {
181            continue;
182        }
183
184        if let Some((name, _)) = part.split_once(":vararg") {
185            let name = name.trim();
186            params.push(Param {
187                name: name.to_string(),
188                default: None,
189                is_vararg: true,
190            });
191        } else if let Some((name, default)) = part.split_once('=') {
192            let name = name.trim();
193            let default = default.trim();
194            params.push(Param {
195                name: name.to_string(),
196                default: Some(default.to_string()),
197                is_vararg: false,
198            });
199        } else {
200            params.push(Param {
201                name: part.to_string(),
202                default: None,
203                is_vararg: false,
204            });
205        }
206    }
207
208    Some(params)
209}
210
211#[cfg(test)]
212mod tests {
213    use {
214        super::*,
215        crate::preprocessor::source_map::{FileId, SourceOrigin},
216    };
217
218    fn make_source_line(text: &str, line: u32) -> SourceLine {
219        SourceLine {
220            text: text.to_string(),
221            origin: SourceOrigin::new(FileId(0), line),
222        }
223    }
224
225    #[test]
226    fn test_parse_macro_directive_simple() {
227        let (name, params) = parse_macro_directive(".macro MY_MACRO").unwrap();
228        assert_eq!(name, "MY_MACRO");
229        assert!(params.is_empty());
230    }
231
232    #[test]
233    fn test_parse_macro_directive_with_params() {
234        let (name, params) = parse_macro_directive(".macro PUSH reg").unwrap();
235        assert_eq!(name, "PUSH");
236        assert_eq!(params.len(), 1);
237        assert_eq!(params[0].name, "reg");
238        assert!(!params[0].is_vararg);
239        assert!(params[0].default.is_none());
240    }
241
242    #[test]
243    fn test_parse_macro_directive_with_defaults() {
244        let (name, params) = parse_macro_directive(".macro FOO a, b=42").unwrap();
245        assert_eq!(name, "FOO");
246        assert_eq!(params.len(), 2);
247        assert_eq!(params[0].name, "a");
248        assert!(params[0].default.is_none());
249        assert_eq!(params[1].name, "b");
250        assert_eq!(params[1].default.as_deref(), Some("42"));
251    }
252
253    #[test]
254    fn test_parse_macro_directive_with_vararg() {
255        let (name, params) = parse_macro_directive(".macro LOG fmt, args:vararg").unwrap();
256        assert_eq!(name, "LOG");
257        assert_eq!(params.len(), 2);
258        assert!(!params[0].is_vararg);
259        assert!(params[1].is_vararg);
260        assert_eq!(params[1].name, "args");
261    }
262
263    #[test]
264    fn test_parse_macro_directive_not_macro() {
265        assert!(parse_macro_directive("mov64 r1, 1").is_none());
266        assert!(parse_macro_directive(".macros FOO").is_none());
267        assert!(parse_macro_directive(".macro").is_none());
268    }
269
270    #[test]
271    fn test_scan_simple_macro() {
272        let lines = vec![
273            make_source_line("before", 1),
274            make_source_line(".macro PUSH reg", 2),
275            make_source_line("    stxdw [r10-8], \\reg", 3),
276            make_source_line("    add64 r10, -8", 4),
277            make_source_line(".endm", 5),
278            make_source_line("after", 6),
279        ];
280
281        let result = scan_macro_definitions(lines);
282        assert!(result.errors.is_empty());
283        assert_eq!(result.macros.len(), 1);
284        assert!(result.macros.contains_key("PUSH"));
285
286        let macro_def = &result.macros["PUSH"];
287        assert_eq!(macro_def.params.len(), 1);
288        assert_eq!(macro_def.params[0].name, "reg");
289        assert_eq!(macro_def.body_lines.len(), 2);
290
291        assert_eq!(result.remaining_lines.len(), 2);
292        assert_eq!(result.remaining_lines[0].text, "before");
293        assert_eq!(result.remaining_lines[1].text, "after");
294    }
295
296    #[test]
297    fn test_scan_unclosed_macro() {
298        let lines = vec![
299            make_source_line(".macro BAD", 1),
300            make_source_line("    body", 2),
301        ];
302
303        let result = scan_macro_definitions(lines);
304        assert_eq!(result.errors.len(), 1);
305        assert!(matches!(
306            &result.errors[0].0,
307            CompileError::UnclosedMacro { name, .. } if name == "BAD"
308        ));
309        // Origin should point to line 1 where .macro was declared
310        assert_eq!(result.errors[0].1.line, 1);
311    }
312
313    #[test]
314    fn test_scan_duplicate_macro() {
315        let lines = vec![
316            make_source_line(".macro FOO", 1),
317            make_source_line(".endm", 2),
318            make_source_line(".macro FOO", 3),
319            make_source_line(".endm", 4),
320        ];
321
322        let result = scan_macro_definitions(lines);
323        assert_eq!(result.errors.len(), 1);
324        assert!(matches!(
325            &result.errors[0].0,
326            CompileError::DuplicateMacroDef { name, .. } if name == "FOO"
327        ));
328    }
329
330    #[test]
331    fn test_scan_vararg_not_last() {
332        let lines = vec![
333            make_source_line(".macro BAD args:vararg, extra", 1),
334            make_source_line(".endm", 2),
335        ];
336
337        let result = scan_macro_definitions(lines);
338        assert_eq!(result.errors.len(), 1);
339        assert!(matches!(
340            &result.errors[0].0,
341            CompileError::VarargNotLast { .. }
342        ));
343    }
344}