Skip to main content

sbpf_assembler/preprocessor/
expand.rs

1use {
2    super::{
3        SourceLine,
4        macro_def::{MacroDef, scan_macro_definitions},
5        source_map::SourceOrigin,
6    },
7    crate::errors::CompileError,
8    std::{
9        collections::HashMap,
10        sync::atomic::{AtomicU64, Ordering},
11    },
12};
13
14const MAX_EXPANSION_DEPTH: u32 = 100;
15
16/// Global expansion counter for `\@` unique IDs
17static EXPANSION_COUNTER: AtomicU64 = AtomicU64::new(0);
18
19/// Reset the expansion counter (for testing)
20#[cfg(test)]
21pub(crate) fn reset_expansion_counter() {
22    EXPANSION_COUNTER.store(0, Ordering::Relaxed);
23}
24
25/// An expansion error paired with its source origin
26#[derive(Debug)]
27pub(crate) struct ExpandError {
28    pub error: CompileError,
29    pub origin: Option<SourceOrigin>,
30}
31
32/// Expand all macros, `.rept`, and `.irp` directives in the given lines.
33///
34/// Returns the expanded lines and any errors encountered.
35pub(crate) fn expand_macros(
36    lines: Vec<SourceLine>,
37) -> Result<(Vec<SourceLine>, Vec<ExpandError>), Vec<ExpandError>> {
38    // Scan for macro definitions and separate them from regular lines
39    let scan_result = scan_macro_definitions(lines);
40
41    if !scan_result.errors.is_empty() {
42        return Err(scan_result
43            .errors
44            .into_iter()
45            .map(|(error, origin)| ExpandError {
46                error,
47                origin: Some(origin),
48            })
49            .collect());
50    }
51
52    let macros = scan_result.macros;
53    let mut errors = Vec::new();
54    let mut output = Vec::new();
55
56    // Expand macro invocations in remaining lines
57    for line in scan_result.remaining_lines {
58        expand_line(&line, &macros, &mut output, &mut errors, 0);
59    }
60
61    // Now, handle .rept and .irp on the post-macro output
62    let output = expand_repetitions(output)?;
63
64    Ok((output, errors))
65}
66
67/// Expand a single line, checking if it's a macro invocation.
68fn expand_line(
69    line: &SourceLine,
70    macros: &HashMap<String, MacroDef>,
71    output: &mut Vec<SourceLine>,
72    errors: &mut Vec<ExpandError>,
73    depth: u32,
74) {
75    if depth > MAX_EXPANSION_DEPTH {
76        errors.push(ExpandError {
77            error: CompileError::MacroRecursionLimit {
78                limit: MAX_EXPANSION_DEPTH,
79                span: 0..0,
80                custom_label: None,
81            },
82            origin: Some(line.origin.clone()),
83        });
84        return;
85    }
86
87    let trimmed = line.text.trim();
88
89    // Check if the first token matches a known macro name
90    let first_token = first_token(trimmed);
91    if let Some(macro_def) = first_token.and_then(|name| macros.get(name)) {
92        let args_str = trimmed[macro_def.name.len()..].trim();
93        let args = split_args(args_str);
94
95        match bind_args(macro_def, &args) {
96            Ok(bindings) => {
97                let expansion_id = EXPANSION_COUNTER.fetch_add(1, Ordering::Relaxed);
98
99                // Expand each body line with parameter substitution
100                for body_line in &macro_def.body_lines {
101                    let expanded_text = substitute(body_line, &bindings, expansion_id);
102                    let expanded_line = SourceLine {
103                        text: expanded_text,
104                        origin: SourceOrigin::with_macro_expansion(
105                            macro_def.defined_at.file_id,
106                            macro_def.defined_at.line,
107                            macro_def.name.clone(),
108                            line.origin.clone(),
109                            depth + 1,
110                        ),
111                    };
112
113                    // Rescan for further macro invocations
114                    expand_line(&expanded_line, macros, output, errors, depth + 1);
115                }
116            }
117            Err(e) => errors.push(ExpandError {
118                error: e,
119                origin: Some(line.origin.clone()),
120            }),
121        }
122    } else {
123        // Not a macro invocation, pass through
124        output.push(line.clone());
125    }
126}
127
128/// Extract the first whitespace-delimited token from a line.
129/// Skips labels (tokens ending with ':').
130fn first_token(line: &str) -> Option<&str> {
131    let trimmed = line.trim();
132    if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with(';') {
133        return None;
134    }
135
136    // If line starts with '.', it's a directive, not a macro
137    if trimmed.starts_with('.') {
138        return None;
139    }
140
141    let end = trimmed
142        .find(|c: char| c.is_whitespace() || c == ',')
143        .unwrap_or(trimmed.len());
144    let token = &trimmed[..end];
145
146    // Skip labels (they end with ':')
147    if token.ends_with(':') {
148        // Check if there's something after the label on the same line
149        let rest = trimmed[end..].trim();
150        if rest.is_empty() {
151            return None;
152        }
153        return first_token(rest);
154    }
155
156    Some(token)
157}
158
159/// Split arguments by top-level commas, respecting quotes and brackets.
160fn split_args(args_str: &str) -> Vec<String> {
161    if args_str.is_empty() {
162        return Vec::new();
163    }
164
165    let mut args = Vec::new();
166    let mut current = String::new();
167    let mut depth = 0i32; // bracket depth
168    let mut in_string = false;
169    let mut escape_next = false;
170
171    for ch in args_str.chars() {
172        if escape_next {
173            current.push(ch);
174            escape_next = false;
175            continue;
176        }
177
178        match ch {
179            '\\' if in_string => {
180                current.push(ch);
181                escape_next = true;
182            }
183            '"' => {
184                in_string = !in_string;
185                current.push(ch);
186            }
187            '(' | '[' if !in_string => {
188                depth += 1;
189                current.push(ch);
190            }
191            ')' | ']' if !in_string => {
192                depth -= 1;
193                current.push(ch);
194            }
195            ',' if !in_string && depth == 0 => {
196                args.push(current.trim().to_string());
197                current = String::new();
198            }
199            _ => {
200                current.push(ch);
201            }
202        }
203    }
204
205    let last = current.trim().to_string();
206    if !last.is_empty() {
207        args.push(last);
208    }
209
210    args
211}
212
213/// Bind arguments to macro parameters, producing a name->value map.
214fn bind_args(
215    macro_def: &MacroDef,
216    args: &[String],
217) -> Result<HashMap<String, String>, CompileError> {
218    let mut bindings = HashMap::new();
219
220    let required_count = macro_def
221        .params
222        .iter()
223        .filter(|p| p.default.is_none() && !p.is_vararg)
224        .count();
225
226    let has_vararg = macro_def.params.iter().any(|p| p.is_vararg);
227
228    // Check argument count
229    if !has_vararg && args.len() > macro_def.params.len() {
230        return Err(CompileError::MacroArgCount {
231            name: macro_def.name.clone(),
232            expected: macro_def.params.len(),
233            got: args.len(),
234            span: 0..0,
235            custom_label: None,
236        });
237    }
238    if args.len() < required_count {
239        return Err(CompileError::MacroArgCount {
240            name: macro_def.name.clone(),
241            expected: required_count,
242            got: args.len(),
243            span: 0..0,
244            custom_label: None,
245        });
246    }
247
248    for (i, param) in macro_def.params.iter().enumerate() {
249        if param.is_vararg {
250            // Collect all remaining arguments
251            let vararg_values: Vec<&str> = args[i..].iter().map(|s| s.as_str()).collect();
252            bindings.insert(param.name.clone(), vararg_values.join(", "));
253        } else if i < args.len() {
254            bindings.insert(param.name.clone(), args[i].clone());
255        } else if let Some(ref default) = param.default {
256            bindings.insert(param.name.clone(), default.clone());
257        }
258        // required params without args already caught above
259    }
260
261    // Also add positional references (\1, \2, etc.)
262    for (i, arg) in args.iter().enumerate() {
263        bindings.insert((i + 1).to_string(), arg.clone());
264    }
265
266    Ok(bindings)
267}
268
269/// Perform parameter substitution on a single body line.
270///
271/// Handles:
272/// - `\name` -> argument value
273/// - `\@` -> unique expansion ID
274/// - `\()` -> zero-width concatenation (disappears after adjacent substitution)
275/// - `\\` -> literal `\`
276fn substitute(line: &str, bindings: &HashMap<String, String>, expansion_id: u64) -> String {
277    let mut result = String::with_capacity(line.len());
278    let chars: Vec<char> = line.chars().collect();
279    let len = chars.len();
280    let mut i = 0;
281
282    while i < len {
283        if chars[i] == '\\' && i + 1 < len {
284            let next = chars[i + 1];
285
286            if next == '\\' {
287                // Escaped backslash
288                result.push('\\');
289                i += 2;
290            } else if next == '@' {
291                // Unique expansion ID
292                result.push_str(&expansion_id.to_string());
293                i += 2;
294            } else if next == '(' && i + 2 < len && chars[i + 2] == ')' {
295                // Token concatenation -- just skip it (zero-width separator)
296                i += 3;
297            } else if next.is_alphanumeric() || next == '_' {
298                // Parameter reference: collect the name
299                let start = i + 1;
300                let mut end = start;
301                while end < len && (chars[end].is_alphanumeric() || chars[end] == '_') {
302                    end += 1;
303                }
304                let param_name: String = chars[start..end].iter().collect();
305
306                if let Some(value) = bindings.get(&param_name) {
307                    result.push_str(value);
308                    // Check if followed by \() for concatenation
309                    if end + 2 < len
310                        && chars[end] == '\\'
311                        && chars[end + 1] == '('
312                        && chars[end + 2] == ')'
313                    {
314                        // Skip the \() separator
315                        end += 3;
316                    }
317                } else {
318                    // Unknown parameter -- keep as-is
319                    result.push('\\');
320                    result.push_str(&param_name);
321                }
322                i = end;
323            } else {
324                // Unknown escape, keep as-is
325                result.push('\\');
326                i += 1;
327            }
328        } else {
329            result.push(chars[i]);
330            i += 1;
331        }
332    }
333
334    result
335}
336
337/// Expand `.rept` and `.irp` blocks (these are processed before macro expansion).
338fn expand_repetitions(lines: Vec<SourceLine>) -> Result<Vec<SourceLine>, Vec<ExpandError>> {
339    let mut output = Vec::new();
340    let mut errors = Vec::new();
341    let mut i = 0;
342
343    while i < lines.len() {
344        let trimmed = lines[i].text.trim();
345
346        if let Some(count_str) = trimmed.strip_prefix(".rept").and_then(|r| {
347            if r.starts_with(char::is_whitespace) {
348                Some(r.trim())
349            } else {
350                None
351            }
352        }) {
353            // Find matching .endr
354            let (body, end_idx) = find_endr_block(&lines, i + 1, &lines[i].origin)?;
355
356            match count_str.parse::<usize>() {
357                Ok(count) => {
358                    // Recursively expand nested .rept/.irp inside the body
359                    let expanded_body = expand_repetitions(body)?;
360                    for _ in 0..count {
361                        output.extend(expanded_body.iter().cloned());
362                    }
363                }
364                Err(_) => {
365                    errors.push(ExpandError {
366                        error: CompileError::InvalidReptCount {
367                            value: count_str.to_string(),
368                            span: 0..0,
369                            custom_label: None,
370                        },
371                        origin: Some(lines[i].origin.clone()),
372                    });
373                }
374            }
375
376            i = end_idx + 1;
377        } else if let Some(rest) = trimmed.strip_prefix(".irp").and_then(|r| {
378            if r.starts_with(char::is_whitespace) {
379                Some(r.trim())
380            } else {
381                None
382            }
383        }) {
384            // Parse: .irp var, val1, val2, val3
385            let (body, end_idx) = find_endr_block(&lines, i + 1, &lines[i].origin)?;
386
387            if let Some((var_name, values_str)) = rest.split_once(',') {
388                let var_name = var_name.trim();
389                let values = split_args(values_str.trim());
390
391                for value in &values {
392                    let mut bindings = HashMap::new();
393                    bindings.insert(var_name.to_string(), value.clone());
394
395                    // Substitute for this iteration first, then recursively
396                    // expand any nested .rept/.irp in the result.
397                    let mut iter_lines = Vec::with_capacity(body.len());
398                    for body_line in &body {
399                        let expansion_id = EXPANSION_COUNTER.fetch_add(1, Ordering::Relaxed);
400                        let expanded_text = substitute(&body_line.text, &bindings, expansion_id);
401                        iter_lines.push(SourceLine {
402                            text: expanded_text,
403                            origin: body_line.origin.clone(),
404                        });
405                    }
406                    output.extend(expand_repetitions(iter_lines)?);
407                }
408            }
409
410            i = end_idx + 1;
411        } else {
412            output.push(lines[i].clone());
413            i += 1;
414        }
415    }
416
417    if errors.is_empty() {
418        Ok(output)
419    } else {
420        Err(errors)
421    }
422}
423
424/// Find the matching `.endr` for a `.rept` or `.irp` block, handling nesting.
425fn find_endr_block(
426    lines: &[SourceLine],
427    start: usize,
428    directive_origin: &SourceOrigin,
429) -> Result<(Vec<SourceLine>, usize), Vec<ExpandError>> {
430    let mut depth = 1u32;
431    let mut i = start;
432
433    while i < lines.len() {
434        let trimmed = lines[i].text.trim();
435        if trimmed == ".endr" {
436            depth -= 1;
437            if depth == 0 {
438                let body = lines[start..i].to_vec();
439                return Ok((body, i));
440            }
441        } else if trimmed.starts_with(".rept") || trimmed.starts_with(".irp") {
442            depth += 1;
443        }
444        i += 1;
445    }
446
447    Err(vec![ExpandError {
448        error: CompileError::UnclosedRept {
449            span: 0..0,
450            custom_label: None,
451        },
452        origin: Some(directive_origin.clone()),
453    }])
454}
455
456#[cfg(test)]
457mod tests {
458    use {super::*, crate::preprocessor::source_map::FileId};
459
460    fn make_line(text: &str, line: u32) -> SourceLine {
461        SourceLine {
462            text: text.to_string(),
463            origin: SourceOrigin::new(FileId(0), line),
464        }
465    }
466
467    fn expand_and_collect(lines: Vec<SourceLine>) -> Vec<String> {
468        let (result, errors) = expand_macros(lines).unwrap();
469        assert!(errors.is_empty(), "Unexpected errors: {:?}", errors);
470        result.iter().map(|l| l.text.clone()).collect()
471    }
472
473    #[test]
474    fn test_first_token() {
475        assert_eq!(first_token("MY_MACRO arg1, arg2"), Some("MY_MACRO"));
476        assert_eq!(first_token("  MY_MACRO  "), Some("MY_MACRO"));
477        assert_eq!(first_token(".globl entry"), None); // directive
478        assert_eq!(first_token("# comment"), None);
479        assert_eq!(first_token(""), None);
480        assert_eq!(first_token("label:"), None);
481        assert_eq!(first_token("label: MY_MACRO"), Some("MY_MACRO"));
482    }
483
484    #[test]
485    fn test_split_args() {
486        assert_eq!(split_args("a, b, c"), vec!["a", "b", "c"]);
487        assert_eq!(split_args("r1, 42"), vec!["r1", "42"]);
488        assert_eq!(
489            split_args("\"hello, world\", 1"),
490            vec!["\"hello, world\"", "1"]
491        );
492        assert_eq!(split_args(""), Vec::<String>::new());
493        assert_eq!(split_args("single"), vec!["single"]);
494    }
495
496    #[test]
497    fn test_substitute_simple() {
498        let mut bindings = HashMap::new();
499        bindings.insert("reg".to_string(), "r1".to_string());
500        bindings.insert("val".to_string(), "42".to_string());
501
502        assert_eq!(
503            substitute("    mov64 \\reg, \\val", &bindings, 0),
504            "    mov64 r1, 42"
505        );
506    }
507
508    #[test]
509    fn test_substitute_unique_id() {
510        let bindings = HashMap::new();
511        assert_eq!(substitute("label_\\@:", &bindings, 5), "label_5:");
512    }
513
514    #[test]
515    fn test_substitute_concatenation() {
516        let mut bindings = HashMap::new();
517        bindings.insert("msg".to_string(), "e1".to_string());
518
519        assert_eq!(substitute("\\msg\\()_end", &bindings, 0), "e1_end");
520    }
521
522    #[test]
523    fn test_substitute_escaped_backslash() {
524        let bindings = HashMap::new();
525        assert_eq!(substitute("\\\\n", &bindings, 0), "\\n");
526    }
527
528    #[test]
529    fn test_simple_macro_expansion() {
530        reset_expansion_counter();
531        let lines = vec![
532            make_line(".macro NOP", 1),
533            make_line("    mov64 r0, 0", 2),
534            make_line(".endm", 3),
535            make_line("NOP", 4),
536        ];
537
538        let result = expand_and_collect(lines);
539        assert_eq!(result, vec!["    mov64 r0, 0"]);
540    }
541
542    #[test]
543    fn test_macro_with_args() {
544        reset_expansion_counter();
545        let lines = vec![
546            make_line(".macro MOV dst, src", 1),
547            make_line("    mov64 \\dst, \\src", 2),
548            make_line(".endm", 3),
549            make_line("MOV r1, r2", 4),
550        ];
551
552        let result = expand_and_collect(lines);
553        assert_eq!(result, vec!["    mov64 r1, r2"]);
554    }
555
556    #[test]
557    fn test_macro_with_default() {
558        reset_expansion_counter();
559        let lines = vec![
560            make_line(".macro SET reg, val=0", 1),
561            make_line("    mov64 \\reg, \\val", 2),
562            make_line(".endm", 3),
563            make_line("SET r1", 4),
564            make_line("SET r2, 42", 5),
565        ];
566
567        let result = expand_and_collect(lines);
568        assert_eq!(result, vec!["    mov64 r1, 0", "    mov64 r2, 42"]);
569    }
570
571    #[test]
572    fn test_macro_unique_id() {
573        reset_expansion_counter();
574        let lines = vec![
575            make_line(".macro LOOP", 1),
576            make_line("loop_\\@:", 2),
577            make_line(".endm", 3),
578            make_line("LOOP", 4),
579            make_line("LOOP", 5),
580        ];
581
582        let result = expand_and_collect(lines);
583        assert_eq!(result.len(), 2);
584        assert!(result.iter().all(|s| {
585            s.starts_with("loop_")
586                && s.ends_with(':')
587                && s["loop_".len()..s.len() - 1].parse::<u64>().is_ok()
588        }));
589        assert_ne!(result[0], result[1]);
590    }
591
592    #[test]
593    fn test_macro_concatenation() {
594        reset_expansion_counter();
595        let lines = vec![
596            make_line(".macro DEF_STR name, text", 1),
597            make_line("\\name:", 2),
598            make_line("    .ascii \\text", 3),
599            make_line("\\name\\()_end:", 4),
600            make_line(".endm", 5),
601            make_line("DEF_STR e1, \"error\"", 6),
602        ];
603
604        let result = expand_and_collect(lines);
605        assert_eq!(result, vec!["e1:", "    .ascii \"error\"", "e1_end:"]);
606    }
607
608    #[test]
609    fn test_nested_macro_expansion() {
610        reset_expansion_counter();
611        let lines = vec![
612            make_line(".macro INNER val", 1),
613            make_line("    mov64 r0, \\val", 2),
614            make_line(".endm", 3),
615            make_line(".macro OUTER val", 4),
616            make_line("INNER \\val", 5),
617            make_line(".endm", 6),
618            make_line("OUTER 42", 7),
619        ];
620
621        let result = expand_and_collect(lines);
622        assert_eq!(result, vec!["    mov64 r0, 42"]);
623    }
624
625    #[test]
626    fn test_recursion_limit() {
627        let lines = vec![
628            make_line(".macro FOREVER", 1),
629            make_line("FOREVER", 2),
630            make_line(".endm", 3),
631            make_line("FOREVER", 4),
632        ];
633
634        let (_, errors) = expand_macros(lines).unwrap();
635        assert!(!errors.is_empty());
636        assert!(matches!(
637            &errors[0].error,
638            CompileError::MacroRecursionLimit { .. }
639        ));
640        // Should have origin pointing to the invocation
641        assert!(errors[0].origin.is_some());
642    }
643
644    #[test]
645    fn test_wrong_arg_count() {
646        let lines = vec![
647            make_line(".macro NEED_TWO a, b", 1),
648            make_line("    mov64 \\a, \\b", 2),
649            make_line(".endm", 3),
650            make_line("NEED_TWO r1", 4),
651        ];
652
653        let (_, errors) = expand_macros(lines).unwrap();
654        assert!(!errors.is_empty());
655        assert!(matches!(
656            &errors[0].error,
657            CompileError::MacroArgCount {
658                expected: 2,
659                got: 1,
660                ..
661            }
662        ));
663        assert!(errors[0].origin.is_some());
664        assert_eq!(errors[0].origin.as_ref().unwrap().line, 4);
665    }
666
667    #[test]
668    fn test_vararg() {
669        reset_expansion_counter();
670        let lines = vec![
671            make_line(".macro LOG fmt, args:vararg", 1),
672            make_line("    .ascii \\fmt", 2),
673            make_line("    .ascii \\args", 3),
674            make_line(".endm", 4),
675            make_line("LOG \"hello\", a, b, c", 5),
676        ];
677
678        let result = expand_and_collect(lines);
679        assert_eq!(result, vec!["    .ascii \"hello\"", "    .ascii a, b, c"]);
680    }
681
682    #[test]
683    fn test_rept() {
684        let lines = vec![
685            make_line(".rept 3", 1),
686            make_line("    nop", 2),
687            make_line(".endr", 3),
688        ];
689
690        let result = expand_and_collect(lines);
691        assert_eq!(result, vec!["    nop", "    nop", "    nop"]);
692    }
693
694    #[test]
695    fn test_rept_zero() {
696        let lines = vec![
697            make_line(".rept 0", 1),
698            make_line("    nop", 2),
699            make_line(".endr", 3),
700        ];
701
702        let result = expand_and_collect(lines);
703        assert!(result.is_empty());
704    }
705
706    #[test]
707    fn test_irp() {
708        reset_expansion_counter();
709        let lines = vec![
710            make_line(".irp reg, r1, r2, r3", 1),
711            make_line("    mov64 \\reg, 0", 2),
712            make_line(".endr", 3),
713        ];
714
715        let result = expand_and_collect(lines);
716        assert_eq!(
717            result,
718            vec!["    mov64 r1, 0", "    mov64 r2, 0", "    mov64 r3, 0"]
719        );
720    }
721
722    #[test]
723    fn test_nested_rept() {
724        let lines = vec![
725            make_line(".rept 2", 1),
726            make_line("    .rept 3", 2),
727            make_line("        mov64 r1, 0x1", 3),
728            make_line("    .endr", 4),
729            make_line(".endr", 5),
730        ];
731
732        let result = expand_and_collect(lines);
733        assert_eq!(result.len(), 6);
734        assert!(result.iter().all(|s| s == "        mov64 r1, 0x1"));
735    }
736
737    #[test]
738    fn test_nested_irp() {
739        reset_expansion_counter();
740        let lines = vec![
741            make_line(".irp reg, r1, r2", 1),
742            make_line("    .irp val, 0x1, 0x2", 2),
743            make_line("        mov64 \\reg, \\val", 3),
744            make_line("    .endr", 4),
745            make_line(".endr", 5),
746        ];
747
748        let result = expand_and_collect(lines);
749        assert_eq!(
750            result,
751            vec![
752                "        mov64 r1, 0x1",
753                "        mov64 r1, 0x2",
754                "        mov64 r2, 0x1",
755                "        mov64 r2, 0x2",
756            ]
757        );
758    }
759
760    #[test]
761    fn test_rept_inside_irp() {
762        reset_expansion_counter();
763        let lines = vec![
764            make_line(".irp r, r1, r2", 1),
765            make_line("    .rept 2", 2),
766            make_line("        mov64 \\r, 0x1", 3),
767            make_line("    .endr", 4),
768            make_line(".endr", 5),
769        ];
770
771        let result = expand_and_collect(lines);
772        assert_eq!(
773            result,
774            vec![
775                "        mov64 r1, 0x1",
776                "        mov64 r1, 0x1",
777                "        mov64 r2, 0x1",
778                "        mov64 r2, 0x1",
779            ]
780        );
781    }
782
783    #[test]
784    fn test_irp_inside_rept() {
785        reset_expansion_counter();
786        let lines = vec![
787            make_line(".rept 2", 1),
788            make_line("    .irp r, r1, r2", 2),
789            make_line("        mov64 \\r, 0x1", 3),
790            make_line("    .endr", 4),
791            make_line(".endr", 5),
792        ];
793
794        let result = expand_and_collect(lines);
795        assert_eq!(
796            result,
797            vec![
798                "        mov64 r1, 0x1",
799                "        mov64 r2, 0x1",
800                "        mov64 r1, 0x1",
801                "        mov64 r2, 0x1",
802            ]
803        );
804    }
805
806    #[test]
807    fn test_rept_count_from_macro_param() {
808        reset_expansion_counter();
809        let lines = vec![
810            make_line(".macro TEST num", 1),
811            make_line("    .rept \\num", 2),
812            make_line("        mov64 r1, 0x123", 3),
813            make_line("    .endr", 4),
814            make_line(".endm", 5),
815            make_line("TEST 3", 7),
816        ];
817
818        let result = expand_and_collect(lines);
819        assert_eq!(
820            result,
821            vec![
822                "        mov64 r1, 0x123",
823                "        mov64 r1, 0x123",
824                "        mov64 r1, 0x123",
825            ]
826        );
827    }
828
829    #[test]
830    fn test_irp_values_from_macro_vararg() {
831        reset_expansion_counter();
832        let lines = vec![
833            make_line(".macro LOAD regs:vararg", 1),
834            make_line("    .irp r, \\regs", 2),
835            make_line("        mov64 \\r, 0x1", 3),
836            make_line("    .endr", 4),
837            make_line(".endm", 5),
838            make_line("LOAD r1, r2, r3", 7),
839        ];
840
841        let result = expand_and_collect(lines);
842        assert_eq!(
843            result,
844            vec![
845                "        mov64 r1, 0x1",
846                "        mov64 r2, 0x1",
847                "        mov64 r3, 0x1",
848            ]
849        );
850    }
851
852    #[test]
853    fn test_unclosed_rept() {
854        let lines = vec![make_line(".rept 3", 1), make_line("    nop", 2)];
855
856        let result = expand_macros(lines);
857        assert!(result.is_err());
858        let errors = result.unwrap_err();
859        assert!(matches!(
860            &errors[0].error,
861            CompileError::UnclosedRept { .. }
862        ));
863        assert!(errors[0].origin.is_some());
864    }
865
866    #[test]
867    fn test_full_example_from_spec() {
868        reset_expansion_counter();
869        let lines = vec![
870            make_line(".macro DEF_STR name, text", 1),
871            make_line("\\name:", 2),
872            make_line("    .ascii \\text", 3),
873            make_line("\\name\\()_end:", 4),
874            make_line(".endm", 5),
875            make_line("", 6),
876            make_line(".macro RETURN_ERR code, msg", 7),
877            make_line("    lddw r0, \\code", 8),
878            make_line("    lddw r1, \\msg", 9),
879            make_line("    lddw r2, \\msg\\()_end - \\msg", 10),
880            make_line("    call sol_log_", 11),
881            make_line("    exit", 12),
882            make_line(".endm", 13),
883            make_line("", 14),
884            make_line("DEF_STR e1, \"error\"", 15),
885            make_line("", 16),
886            make_line("RETURN_ERR 1, e1", 17),
887        ];
888
889        let result = expand_and_collect(lines);
890        assert_eq!(
891            result,
892            vec![
893                "", // blank line between definitions
894                "", // blank line after definitions
895                "e1:",
896                "    .ascii \"error\"",
897                "e1_end:",
898                "", // blank line
899                "    lddw r0, 1",
900                "    lddw r1, e1",
901                "    lddw r2, e1_end - e1",
902                "    call sol_log_",
903                "    exit",
904            ]
905        );
906    }
907
908    #[test]
909    fn test_passthrough_no_macros() {
910        let lines = vec![
911            make_line(".globl entrypoint", 1),
912            make_line("entrypoint:", 2),
913            make_line("    mov64 r1, 42", 3),
914            make_line("    exit", 4),
915        ];
916
917        let result = expand_and_collect(lines);
918        assert_eq!(
919            result,
920            vec![
921                ".globl entrypoint",
922                "entrypoint:",
923                "    mov64 r1, 42",
924                "    exit",
925            ]
926        );
927    }
928}