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/// An expansion error paired with its source origin
20#[derive(Debug)]
21pub(crate) struct ExpandError {
22    pub error: CompileError,
23    pub origin: Option<SourceOrigin>,
24}
25
26/// Expand all macros, `.rept`, `.irp`, and `.if` directives in the given lines.
27///
28/// Returns the expanded lines and any errors encountered.
29pub(crate) fn expand_macros(
30    lines: Vec<SourceLine>,
31) -> Result<(Vec<SourceLine>, Vec<ExpandError>), Vec<ExpandError>> {
32    // Scan for macro definitions and separate them from regular lines
33    let scan_result = scan_macro_definitions(lines);
34
35    if !scan_result.errors.is_empty() {
36        return Err(scan_result
37            .errors
38            .into_iter()
39            .map(|(error, origin)| ExpandError {
40                error,
41                origin: Some(origin),
42            })
43            .collect());
44    }
45
46    let macros = scan_result.macros;
47    let mut errors = Vec::new();
48    let mut output = Vec::new();
49
50    // Expand macro invocations in remaining lines
51    for line in scan_result.remaining_lines {
52        expand_line(&line, &macros, &mut output, &mut errors, 0);
53    }
54
55    // Now, handle .rept, .irp, and .if on the post-macro output
56    let output = expand_repetitions(output)?;
57
58    Ok((output, errors))
59}
60
61/// Expand a single line, checking if it's a macro invocation.
62fn expand_line(
63    line: &SourceLine,
64    macros: &HashMap<String, MacroDef>,
65    output: &mut Vec<SourceLine>,
66    errors: &mut Vec<ExpandError>,
67    depth: u32,
68) {
69    if depth > MAX_EXPANSION_DEPTH {
70        errors.push(ExpandError {
71            error: CompileError::MacroRecursionLimit {
72                limit: MAX_EXPANSION_DEPTH,
73                span: 0..0,
74                custom_label: None,
75            },
76            origin: Some(line.origin.clone()),
77        });
78        return;
79    }
80
81    let trimmed = line.text.trim();
82
83    // Check if the first token matches a known macro name
84    let first_token = first_token(trimmed);
85    if let Some(macro_def) = first_token.and_then(|name| macros.get(name)) {
86        let args_str = trimmed[macro_def.name.len()..].trim();
87        let args = split_args(args_str);
88
89        match bind_args(macro_def, &args) {
90            Ok(bindings) => {
91                let expansion_id = EXPANSION_COUNTER.fetch_add(1, Ordering::Relaxed);
92
93                // Expand each body line with parameter substitution
94                for body_line in &macro_def.body_lines {
95                    let expanded_text = substitute(body_line, &bindings, expansion_id);
96                    let expanded_line = SourceLine {
97                        text: expanded_text,
98                        origin: SourceOrigin::with_macro_expansion(
99                            macro_def.defined_at.file_id,
100                            macro_def.defined_at.line,
101                            macro_def.name.clone(),
102                            line.origin.clone(),
103                            depth + 1,
104                        ),
105                    };
106
107                    // Rescan for further macro invocations
108                    expand_line(&expanded_line, macros, output, errors, depth + 1);
109                }
110            }
111            Err(e) => errors.push(ExpandError {
112                error: e,
113                origin: Some(line.origin.clone()),
114            }),
115        }
116    } else {
117        // Not a macro invocation, pass through
118        output.push(line.clone());
119    }
120}
121
122/// Extract the first whitespace-delimited token from a line.
123/// Skips labels (tokens ending with ':').
124fn first_token(line: &str) -> Option<&str> {
125    let trimmed = line.trim();
126    if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with(';') {
127        return None;
128    }
129
130    // If line starts with '.', it's a directive, not a macro
131    if trimmed.starts_with('.') {
132        return None;
133    }
134
135    let end = trimmed
136        .find(|c: char| c.is_whitespace() || c == ',')
137        .unwrap_or(trimmed.len());
138    let token = &trimmed[..end];
139
140    // Skip labels (they end with ':')
141    if token.ends_with(':') {
142        // Check if there's something after the label on the same line
143        let rest = trimmed[end..].trim();
144        if rest.is_empty() {
145            return None;
146        }
147        return first_token(rest);
148    }
149
150    Some(token)
151}
152
153/// Split arguments by top-level commas, respecting quotes and brackets.
154fn split_args(args_str: &str) -> Vec<String> {
155    if args_str.is_empty() {
156        return Vec::new();
157    }
158
159    let mut args = Vec::new();
160    let mut current = String::new();
161    let mut depth = 0i32; // bracket depth
162    let mut in_string = false;
163    let mut escape_next = false;
164
165    for ch in args_str.chars() {
166        if escape_next {
167            current.push(ch);
168            escape_next = false;
169            continue;
170        }
171
172        match ch {
173            '\\' if in_string => {
174                current.push(ch);
175                escape_next = true;
176            }
177            '"' => {
178                in_string = !in_string;
179                current.push(ch);
180            }
181            '(' | '[' if !in_string => {
182                depth += 1;
183                current.push(ch);
184            }
185            ')' | ']' if !in_string => {
186                depth -= 1;
187                current.push(ch);
188            }
189            ',' if !in_string && depth == 0 => {
190                args.push(current.trim().to_string());
191                current = String::new();
192            }
193            _ => {
194                current.push(ch);
195            }
196        }
197    }
198
199    let last = current.trim().to_string();
200    if !last.is_empty() {
201        args.push(last);
202    }
203
204    args
205}
206
207/// Bind arguments to macro parameters, producing a name->value map.
208fn bind_args(
209    macro_def: &MacroDef,
210    args: &[String],
211) -> Result<HashMap<String, String>, CompileError> {
212    let mut bindings = HashMap::new();
213
214    let required_count = macro_def
215        .params
216        .iter()
217        .filter(|p| p.default.is_none() && !p.is_vararg)
218        .count();
219
220    let has_vararg = macro_def.params.iter().any(|p| p.is_vararg);
221
222    // Check argument count
223    if !has_vararg && args.len() > macro_def.params.len() {
224        return Err(CompileError::MacroArgCount {
225            name: macro_def.name.clone(),
226            expected: macro_def.params.len(),
227            got: args.len(),
228            span: 0..0,
229            custom_label: None,
230        });
231    }
232    if args.len() < required_count {
233        return Err(CompileError::MacroArgCount {
234            name: macro_def.name.clone(),
235            expected: required_count,
236            got: args.len(),
237            span: 0..0,
238            custom_label: None,
239        });
240    }
241
242    for (i, param) in macro_def.params.iter().enumerate() {
243        if param.is_vararg {
244            // Collect all remaining arguments
245            let vararg_values: Vec<&str> = args[i..].iter().map(|s| s.as_str()).collect();
246            bindings.insert(param.name.clone(), vararg_values.join(", "));
247        } else if i < args.len() {
248            bindings.insert(param.name.clone(), args[i].clone());
249        } else if let Some(ref default) = param.default {
250            bindings.insert(param.name.clone(), default.clone());
251        }
252        // required params without args already caught above
253    }
254
255    // Also add positional references (\1, \2, etc.)
256    for (i, arg) in args.iter().enumerate() {
257        bindings.insert((i + 1).to_string(), arg.clone());
258    }
259
260    Ok(bindings)
261}
262
263/// Perform parameter substitution on a single body line.
264///
265/// Handles:
266/// - `\name` -> argument value
267/// - `\@` -> unique expansion ID
268/// - `\()` -> zero-width concatenation (disappears after adjacent substitution)
269/// - `\\` -> literal `\`
270fn substitute(line: &str, bindings: &HashMap<String, String>, expansion_id: u64) -> String {
271    let mut result = String::with_capacity(line.len());
272    let chars: Vec<char> = line.chars().collect();
273    let len = chars.len();
274    let mut i = 0;
275
276    while i < len {
277        if chars[i] == '\\' && i + 1 < len {
278            let next = chars[i + 1];
279
280            if next == '\\' {
281                // Escaped backslash
282                result.push('\\');
283                i += 2;
284            } else if next == '@' {
285                // Unique expansion ID
286                result.push_str(&expansion_id.to_string());
287                i += 2;
288            } else if next == '(' && i + 2 < len && chars[i + 2] == ')' {
289                // Token concatenation -- just skip it (zero-width separator)
290                i += 3;
291            } else if next.is_alphanumeric() || next == '_' {
292                // Parameter reference: collect the name
293                let start = i + 1;
294                let mut end = start;
295                while end < len && (chars[end].is_alphanumeric() || chars[end] == '_') {
296                    end += 1;
297                }
298                let param_name: String = chars[start..end].iter().collect();
299
300                if let Some(value) = bindings.get(&param_name) {
301                    result.push_str(value);
302                    // Check if followed by \() for concatenation
303                    if end + 2 < len
304                        && chars[end] == '\\'
305                        && chars[end + 1] == '('
306                        && chars[end + 2] == ')'
307                    {
308                        // Skip the \() separator
309                        end += 3;
310                    }
311                } else {
312                    // Unknown parameter -- keep as-is
313                    result.push('\\');
314                    result.push_str(&param_name);
315                }
316                i = end;
317            } else {
318                // Unknown escape, keep as-is
319                result.push('\\');
320                i += 1;
321            }
322        } else {
323            result.push(chars[i]);
324            i += 1;
325        }
326    }
327
328    result
329}
330
331/// If `trimmed` is the directive `name` followed by an argument, return the
332/// trimmed argument (e.g. `directive_arg(".rept 3", ".rept") == Some("3")`).
333fn directive_arg<'a>(trimmed: &'a str, name: &str) -> Option<&'a str> {
334    trimmed
335        .strip_prefix(name)
336        .and_then(|r| r.starts_with(char::is_whitespace).then(|| r.trim()))
337}
338
339/// Expand `.rept`, `.irp`, and `.if`/`.else`/`.endif` blocks
340/// (these are processed after macro expansion).
341fn expand_repetitions(lines: Vec<SourceLine>) -> Result<Vec<SourceLine>, Vec<ExpandError>> {
342    let mut output = Vec::new();
343    let mut errors = Vec::new();
344    let mut i = 0;
345
346    while i < lines.len() {
347        let trimmed = lines[i].text.trim();
348
349        if let Some(count_str) = directive_arg(trimmed, ".rept") {
350            // Find matching .endr
351            let (body, end_idx) = find_endr_block(&lines, i + 1, &lines[i].origin)?;
352
353            match eval_rept_count(count_str) {
354                Some(count) => {
355                    // Recursively expand nested .rept/.irp inside the body
356                    let expanded_body = expand_repetitions(body)?;
357                    for _ in 0..count {
358                        output.extend(expanded_body.iter().cloned());
359                    }
360                }
361                None => {
362                    errors.push(ExpandError {
363                        error: CompileError::InvalidReptCount {
364                            value: count_str.to_string(),
365                            span: 0..0,
366                            custom_label: None,
367                        },
368                        origin: Some(lines[i].origin.clone()),
369                    });
370                }
371            }
372
373            i = end_idx + 1;
374        } else if let Some(rest) = directive_arg(trimmed, ".irp") {
375            // Parse: .irp var, val1, val2, val3
376            let (body, end_idx) = find_endr_block(&lines, i + 1, &lines[i].origin)?;
377
378            if let Some((var_name, values_str)) = rest.split_once(',') {
379                let var_name = var_name.trim();
380                let values = split_args(values_str.trim());
381
382                for value in &values {
383                    let mut bindings = HashMap::new();
384                    bindings.insert(var_name.to_string(), value.clone());
385
386                    // Substitute for this iteration first, then recursively
387                    // expand any nested .rept/.irp in the result.
388                    let mut iter_lines = Vec::with_capacity(body.len());
389                    for body_line in &body {
390                        let expansion_id = EXPANSION_COUNTER.fetch_add(1, Ordering::Relaxed);
391                        let expanded_text = substitute(&body_line.text, &bindings, expansion_id);
392                        iter_lines.push(SourceLine {
393                            text: expanded_text,
394                            origin: body_line.origin.clone(),
395                        });
396                    }
397                    output.extend(expand_repetitions(iter_lines)?);
398                }
399            }
400
401            i = end_idx + 1;
402        } else if let Some(cond_str) = directive_arg(trimmed, ".if") {
403            let (then_body, else_body, end_idx) = find_if_block(&lines, i + 1, &lines[i].origin)?;
404
405            match eval_if_condition(cond_str) {
406                Some(taken) => {
407                    let body = if taken { then_body } else { else_body };
408                    output.extend(expand_repetitions(body)?);
409                }
410                None => {
411                    errors.push(ExpandError {
412                        error: CompileError::InvalidIfCondition {
413                            value: cond_str.to_string(),
414                            span: 0..0,
415                            custom_label: None,
416                        },
417                        origin: Some(lines[i].origin.clone()),
418                    });
419                }
420            }
421
422            i = end_idx + 1;
423        } else if trimmed == ".else" || trimmed == ".endif" {
424            let error = if trimmed == ".else" {
425                CompileError::StrayElse {
426                    span: 0..0,
427                    custom_label: None,
428                }
429            } else {
430                CompileError::StrayEndif {
431                    span: 0..0,
432                    custom_label: None,
433                }
434            };
435            errors.push(ExpandError {
436                error,
437                origin: Some(lines[i].origin.clone()),
438            });
439            i += 1;
440        } else {
441            output.push(lines[i].clone());
442            i += 1;
443        }
444    }
445
446    if errors.is_empty() {
447        Ok(output)
448    } else {
449        Err(errors)
450    }
451}
452
453/// Evaluate a `.rept` count: a constant expression of number literals,
454/// parentheses, unary minus, and `+ - * /`. Returns `None` on syntax errors,
455/// overflow, division by zero, or a negative result.
456fn eval_rept_count(s: &str) -> Option<usize> {
457    usize::try_from(eval_const(s)?).ok()
458}
459
460/// Evaluate an expression, returning its value and the unconsumed remainder
461/// (with leading whitespace already stripped).
462fn eval_const_expr(s: &str) -> Option<(i64, &str)> {
463    let (mut acc, mut rest) = eval_const_term(s)?;
464    loop {
465        let r = rest.trim_start();
466        let Some(op) = r
467            .chars()
468            .next()
469            .filter(|c| matches!(c, '+' | '-' | '*' | '/'))
470        else {
471            return Some((acc, r));
472        };
473        let (rhs, r) = eval_const_term(&r[1..])?;
474        acc = match op {
475            '+' => acc.checked_add(rhs)?,
476            '-' => acc.checked_sub(rhs)?,
477            '*' => acc.checked_mul(rhs)?,
478            _ => acc.checked_div(rhs)?,
479        };
480        rest = r;
481    }
482}
483
484/// Evaluate a parenthesised expression or a single number literal: decimal or
485/// `0x`-prefixed hex, optionally negated, with `_` separators ignored.
486fn eval_const_term(s: &str) -> Option<(i64, &str)> {
487    let s = s.trim_start();
488    if let Some(r) = s.strip_prefix('(') {
489        let (value, r) = eval_const_expr(r)?;
490        return Some((value, r.strip_prefix(')')?));
491    }
492    let (negative, s) = match s.strip_prefix('-') {
493        Some(r) => (true, r.trim_start()),
494        None => (false, s),
495    };
496    let (radix, s) = match s.strip_prefix("0x") {
497        Some(r) => (16, r),
498        None => (10, s),
499    };
500    let end = s
501        .find(|c: char| !c.is_digit(radix) && c != '_')
502        .unwrap_or(s.len());
503    let value = i64::from_str_radix(&s[..end].replace('_', ""), radix).ok()?;
504    Some((if negative { -value } else { value }, &s[end..]))
505}
506
507/// Evaluate a `.if` condition: an optional comparison (`== != <= >= < >`)
508/// between two constant expressions, or a bare expression (non-zero = true).
509fn eval_if_condition(s: &str) -> Option<bool> {
510    for op in ["==", "!=", "<=", ">=", "<", ">"] {
511        if let Some((lhs, rhs)) = s.split_once(op) {
512            let (lhs, rhs) = (eval_const(lhs)?, eval_const(rhs)?);
513            return Some(match op {
514                "==" => lhs == rhs,
515                "!=" => lhs != rhs,
516                "<=" => lhs <= rhs,
517                ">=" => lhs >= rhs,
518                "<" => lhs < rhs,
519                _ => lhs > rhs,
520            });
521        }
522    }
523    eval_const(s).map(|v| v != 0)
524}
525
526/// Evaluate an expression that must consume its entire input.
527fn eval_const(s: &str) -> Option<i64> {
528    let (value, rest) = eval_const_expr(s)?;
529    rest.trim().is_empty().then_some(value)
530}
531
532/// Is this line a `.if <expr>` directive?
533fn is_if_directive(trimmed: &str) -> bool {
534    directive_arg(trimmed, ".if").is_some()
535}
536
537/// Find the matching `.else`/`.endif` for a `.if` block, handling nesting.
538/// Returns (then_body, else_body, endif_idx).
539#[allow(clippy::type_complexity)]
540fn find_if_block(
541    lines: &[SourceLine],
542    start: usize,
543    directive_origin: &SourceOrigin,
544) -> Result<(Vec<SourceLine>, Vec<SourceLine>, usize), Vec<ExpandError>> {
545    let mut depth = 1u32;
546    let mut else_idx = None;
547
548    for i in start..lines.len() {
549        let trimmed = lines[i].text.trim();
550        if is_if_directive(trimmed) {
551            depth += 1;
552        } else if trimmed == ".endif" {
553            depth -= 1;
554            if depth == 0 {
555                let split = else_idx.unwrap_or(i);
556                let then_body = lines[start..split].to_vec();
557                let else_body = else_idx.map_or(Vec::new(), |e| lines[e + 1..i].to_vec());
558                return Ok((then_body, else_body, i));
559            }
560        } else if trimmed == ".else" && depth == 1 {
561            if else_idx.is_some() {
562                return Err(vec![ExpandError {
563                    error: CompileError::DuplicateElse {
564                        span: 0..0,
565                        custom_label: None,
566                    },
567                    origin: Some(lines[i].origin.clone()),
568                }]);
569            }
570            else_idx = Some(i);
571        }
572    }
573
574    Err(vec![ExpandError {
575        error: CompileError::UnclosedIf {
576            span: 0..0,
577            custom_label: None,
578        },
579        origin: Some(directive_origin.clone()),
580    }])
581}
582
583/// Find the matching `.endr` for a `.rept` or `.irp` block, handling nesting.
584fn find_endr_block(
585    lines: &[SourceLine],
586    start: usize,
587    directive_origin: &SourceOrigin,
588) -> Result<(Vec<SourceLine>, usize), Vec<ExpandError>> {
589    let mut depth = 1u32;
590    let mut i = start;
591
592    while i < lines.len() {
593        let trimmed = lines[i].text.trim();
594        if trimmed == ".endr" {
595            depth -= 1;
596            if depth == 0 {
597                let body = lines[start..i].to_vec();
598                return Ok((body, i));
599            }
600        } else if trimmed.starts_with(".rept") || trimmed.starts_with(".irp") {
601            depth += 1;
602        }
603        i += 1;
604    }
605
606    Err(vec![ExpandError {
607        error: CompileError::UnclosedRept {
608            span: 0..0,
609            custom_label: None,
610        },
611        origin: Some(directive_origin.clone()),
612    }])
613}
614
615#[cfg(test)]
616mod tests {
617    use {super::*, crate::preprocessor::source_map::FileId};
618
619    fn make_line(text: &str, line: u32) -> SourceLine {
620        SourceLine {
621            text: text.to_string(),
622            origin: SourceOrigin::new(FileId(0), line),
623        }
624    }
625
626    fn expand_and_collect(lines: Vec<SourceLine>) -> Vec<String> {
627        let (result, errors) = expand_macros(lines).unwrap();
628        assert!(errors.is_empty(), "Unexpected errors: {:?}", errors);
629        result.iter().map(|l| l.text.clone()).collect()
630    }
631
632    #[test]
633    fn test_first_token() {
634        assert_eq!(first_token("MY_MACRO arg1, arg2"), Some("MY_MACRO"));
635        assert_eq!(first_token("  MY_MACRO  "), Some("MY_MACRO"));
636        assert_eq!(first_token(".globl entry"), None); // directive
637        assert_eq!(first_token("# comment"), None);
638        assert_eq!(first_token(""), None);
639        assert_eq!(first_token("label:"), None);
640        assert_eq!(first_token("label: MY_MACRO"), Some("MY_MACRO"));
641    }
642
643    #[test]
644    fn test_split_args() {
645        assert_eq!(split_args("a, b, c"), vec!["a", "b", "c"]);
646        assert_eq!(split_args("r1, 42"), vec!["r1", "42"]);
647        assert_eq!(
648            split_args("\"hello, world\", 1"),
649            vec!["\"hello, world\"", "1"]
650        );
651        assert_eq!(split_args(""), Vec::<String>::new());
652        assert_eq!(split_args("single"), vec!["single"]);
653    }
654
655    #[test]
656    fn test_substitute_simple() {
657        let mut bindings = HashMap::new();
658        bindings.insert("reg".to_string(), "r1".to_string());
659        bindings.insert("val".to_string(), "42".to_string());
660
661        assert_eq!(
662            substitute("    mov64 \\reg, \\val", &bindings, 0),
663            "    mov64 r1, 42"
664        );
665    }
666
667    #[test]
668    fn test_substitute_unique_id() {
669        let bindings = HashMap::new();
670        assert_eq!(substitute("label_\\@:", &bindings, 5), "label_5:");
671    }
672
673    #[test]
674    fn test_substitute_concatenation() {
675        let mut bindings = HashMap::new();
676        bindings.insert("msg".to_string(), "e1".to_string());
677
678        assert_eq!(substitute("\\msg\\()_end", &bindings, 0), "e1_end");
679    }
680
681    #[test]
682    fn test_substitute_escaped_backslash() {
683        let bindings = HashMap::new();
684        assert_eq!(substitute("\\\\n", &bindings, 0), "\\n");
685    }
686
687    #[test]
688    fn test_simple_macro_expansion() {
689        let lines = vec![
690            make_line(".macro NOP", 1),
691            make_line("    mov64 r0, 0", 2),
692            make_line(".endm", 3),
693            make_line("NOP", 4),
694        ];
695
696        let result = expand_and_collect(lines);
697        assert_eq!(result, vec!["    mov64 r0, 0"]);
698    }
699
700    #[test]
701    fn test_macro_with_args() {
702        let lines = vec![
703            make_line(".macro MOV dst, src", 1),
704            make_line("    mov64 \\dst, \\src", 2),
705            make_line(".endm", 3),
706            make_line("MOV r1, r2", 4),
707        ];
708
709        let result = expand_and_collect(lines);
710        assert_eq!(result, vec!["    mov64 r1, r2"]);
711    }
712
713    #[test]
714    fn test_macro_with_default() {
715        let lines = vec![
716            make_line(".macro SET reg, val=0", 1),
717            make_line("    mov64 \\reg, \\val", 2),
718            make_line(".endm", 3),
719            make_line("SET r1", 4),
720            make_line("SET r2, 42", 5),
721        ];
722
723        let result = expand_and_collect(lines);
724        assert_eq!(result, vec!["    mov64 r1, 0", "    mov64 r2, 42"]);
725    }
726
727    #[test]
728    fn test_macro_unique_id() {
729        let lines = vec![
730            make_line(".macro LOOP", 1),
731            make_line("loop_\\@:", 2),
732            make_line(".endm", 3),
733            make_line("LOOP", 4),
734            make_line("LOOP", 5),
735        ];
736
737        let result = expand_and_collect(lines);
738        assert_eq!(result.len(), 2);
739        assert!(result.iter().all(|s| {
740            s.starts_with("loop_")
741                && s.ends_with(':')
742                && s["loop_".len()..s.len() - 1].parse::<u64>().is_ok()
743        }));
744        assert_ne!(result[0], result[1]);
745    }
746
747    #[test]
748    fn test_macro_concatenation() {
749        let lines = vec![
750            make_line(".macro DEF_STR name, text", 1),
751            make_line("\\name:", 2),
752            make_line("    .ascii \\text", 3),
753            make_line("\\name\\()_end:", 4),
754            make_line(".endm", 5),
755            make_line("DEF_STR e1, \"error\"", 6),
756        ];
757
758        let result = expand_and_collect(lines);
759        assert_eq!(result, vec!["e1:", "    .ascii \"error\"", "e1_end:"]);
760    }
761
762    #[test]
763    fn test_nested_macro_expansion() {
764        let lines = vec![
765            make_line(".macro INNER val", 1),
766            make_line("    mov64 r0, \\val", 2),
767            make_line(".endm", 3),
768            make_line(".macro OUTER val", 4),
769            make_line("INNER \\val", 5),
770            make_line(".endm", 6),
771            make_line("OUTER 42", 7),
772        ];
773
774        let result = expand_and_collect(lines);
775        assert_eq!(result, vec!["    mov64 r0, 42"]);
776    }
777
778    #[test]
779    fn test_recursion_limit() {
780        let lines = vec![
781            make_line(".macro FOREVER", 1),
782            make_line("FOREVER", 2),
783            make_line(".endm", 3),
784            make_line("FOREVER", 4),
785        ];
786
787        let (_, errors) = expand_macros(lines).unwrap();
788        assert!(!errors.is_empty());
789        assert!(matches!(
790            &errors[0].error,
791            CompileError::MacroRecursionLimit { .. }
792        ));
793        // Should have origin pointing to the invocation
794        assert!(errors[0].origin.is_some());
795    }
796
797    #[test]
798    fn test_wrong_arg_count() {
799        let lines = vec![
800            make_line(".macro NEED_TWO a, b", 1),
801            make_line("    mov64 \\a, \\b", 2),
802            make_line(".endm", 3),
803            make_line("NEED_TWO r1", 4),
804        ];
805
806        let (_, errors) = expand_macros(lines).unwrap();
807        assert!(!errors.is_empty());
808        assert!(matches!(
809            &errors[0].error,
810            CompileError::MacroArgCount {
811                expected: 2,
812                got: 1,
813                ..
814            }
815        ));
816        assert!(errors[0].origin.is_some());
817        assert_eq!(errors[0].origin.as_ref().unwrap().line, 4);
818    }
819
820    #[test]
821    fn test_vararg() {
822        let lines = vec![
823            make_line(".macro LOG fmt, args:vararg", 1),
824            make_line("    .ascii \\fmt", 2),
825            make_line("    .ascii \\args", 3),
826            make_line(".endm", 4),
827            make_line("LOG \"hello\", a, b, c", 5),
828        ];
829
830        let result = expand_and_collect(lines);
831        assert_eq!(result, vec!["    .ascii \"hello\"", "    .ascii a, b, c"]);
832    }
833
834    #[test]
835    fn test_rept() {
836        let lines = vec![
837            make_line(".rept 3", 1),
838            make_line("    nop", 2),
839            make_line(".endr", 3),
840        ];
841
842        let result = expand_and_collect(lines);
843        assert_eq!(result, vec!["    nop", "    nop", "    nop"]);
844    }
845
846    #[test]
847    fn test_eval_rept_count() {
848        assert_eq!(eval_rept_count("3"), Some(3));
849        assert_eq!(eval_rept_count("0x10"), Some(16));
850        assert_eq!(eval_rept_count("1_000"), Some(1000));
851        assert_eq!(eval_rept_count("2 * 4"), Some(8));
852        assert_eq!(eval_rept_count("(1 + 2) * 2"), Some(6));
853        assert_eq!(eval_rept_count("4 - 1"), Some(3));
854        assert_eq!(eval_rept_count("6 / 2"), Some(3));
855        assert_eq!(eval_rept_count("2 - 4"), None); // negative
856        assert_eq!(eval_rept_count("1 / 0"), None);
857        assert_eq!(eval_rept_count("foo"), None);
858        assert_eq!(eval_rept_count("2 +"), None);
859        assert_eq!(eval_rept_count("(1"), None);
860    }
861
862    #[test]
863    fn test_rept_expression_from_macro_param() {
864        let lines = vec![
865            make_line(".macro FILL num", 1),
866            make_line("    .rept \\num + 1", 2),
867            make_line("        nop", 3),
868            make_line("    .endr", 4),
869            make_line(".endm", 5),
870            make_line("FILL 2", 6),
871        ];
872
873        let result = expand_and_collect(lines);
874        assert_eq!(result, vec!["        nop", "        nop", "        nop"]);
875    }
876
877    #[test]
878    fn test_rept_invalid_count() {
879        let lines = vec![
880            make_line(".rept foo", 1),
881            make_line("    nop", 2),
882            make_line(".endr", 3),
883        ];
884
885        let errors = expand_macros(lines).unwrap_err();
886        assert!(matches!(
887            &errors[0].error,
888            CompileError::InvalidReptCount { value, .. } if value == "foo"
889        ));
890    }
891
892    #[test]
893    fn test_eval_if_condition() {
894        assert_eq!(eval_if_condition("1"), Some(true));
895        assert_eq!(eval_if_condition("0"), Some(false));
896        assert_eq!(eval_if_condition("2 - 2"), Some(false));
897        assert_eq!(eval_if_condition("3 == 3"), Some(true));
898        assert_eq!(eval_if_condition("3 != 3"), Some(false));
899        assert_eq!(eval_if_condition("2 < 3"), Some(true));
900        assert_eq!(eval_if_condition("2 >= 3"), Some(false));
901        assert_eq!(eval_if_condition("0x10 <= 16"), Some(true));
902        assert_eq!(eval_if_condition("foo"), None);
903        assert_eq!(eval_if_condition("1 == foo"), None);
904    }
905
906    #[test]
907    fn test_if_true_branch() {
908        let lines = vec![
909            make_line(".if 1", 1),
910            make_line("    mov64 r0, 1", 2),
911            make_line(".else", 3),
912            make_line("    mov64 r0, 2", 4),
913            make_line(".endif", 5),
914        ];
915
916        let result = expand_and_collect(lines);
917        assert_eq!(result, vec!["    mov64 r0, 1"]);
918    }
919
920    #[test]
921    fn test_if_false_branch() {
922        let lines = vec![
923            make_line(".if 2 > 3", 1),
924            make_line("    mov64 r0, 1", 2),
925            make_line(".else", 3),
926            make_line("    mov64 r0, 2", 4),
927            make_line(".endif", 5),
928        ];
929
930        let result = expand_and_collect(lines);
931        assert_eq!(result, vec!["    mov64 r0, 2"]);
932    }
933
934    #[test]
935    fn test_if_false_no_else() {
936        let lines = vec![
937            make_line(".if 0", 1),
938            make_line("    mov64 r0, 1", 2),
939            make_line(".endif", 3),
940        ];
941
942        let result = expand_and_collect(lines);
943        assert!(result.is_empty());
944    }
945
946    #[test]
947    fn test_nested_if() {
948        let lines = vec![
949            make_line(".if 1", 1),
950            make_line("    .if 0", 2),
951            make_line("        nop1", 3),
952            make_line("    .else", 4),
953            make_line("        nop2", 5),
954            make_line("    .endif", 6),
955            make_line(".else", 7),
956            make_line("    nop3", 8),
957            make_line(".endif", 9),
958        ];
959
960        let result = expand_and_collect(lines);
961        assert_eq!(result, vec!["        nop2"]);
962    }
963
964    #[test]
965    fn test_if_from_macro_param() {
966        let lines = vec![
967            make_line(".macro SET_OR_NOP val", 1),
968            make_line("    .if \\val == 0", 2),
969            make_line("        mov64 r0, 0", 3),
970            make_line("    .else", 4),
971            make_line("        mov64 r0, \\val", 5),
972            make_line("    .endif", 6),
973            make_line(".endm", 7),
974            make_line("SET_OR_NOP 0", 8),
975            make_line("SET_OR_NOP 5", 9),
976        ];
977
978        let result = expand_and_collect(lines);
979        assert_eq!(result, vec!["        mov64 r0, 0", "        mov64 r0, 5"]);
980    }
981
982    #[test]
983    fn test_if_inside_irp() {
984        let lines = vec![
985            make_line(".irp v, 0, 1, 2", 1),
986            make_line("    .if \\v != 1", 2),
987            make_line("        mov64 r0, \\v", 3),
988            make_line("    .endif", 4),
989            make_line(".endr", 5),
990        ];
991
992        let result = expand_and_collect(lines);
993        assert_eq!(result, vec!["        mov64 r0, 0", "        mov64 r0, 2"]);
994    }
995
996    #[test]
997    fn test_rept_inside_if() {
998        let lines = vec![
999            make_line(".if 1", 1),
1000            make_line("    .rept 2", 2),
1001            make_line("        nop", 3),
1002            make_line("    .endr", 4),
1003            make_line(".endif", 5),
1004        ];
1005
1006        let result = expand_and_collect(lines);
1007        assert_eq!(result, vec!["        nop", "        nop"]);
1008    }
1009
1010    #[test]
1011    fn test_unclosed_if() {
1012        let lines = vec![make_line(".if 1", 1), make_line("    nop", 2)];
1013
1014        let errors = expand_macros(lines).unwrap_err();
1015        assert!(matches!(&errors[0].error, CompileError::UnclosedIf { .. }));
1016        assert_eq!(errors[0].origin.as_ref().unwrap().line, 1);
1017    }
1018
1019    #[test]
1020    fn test_invalid_if_condition() {
1021        let lines = vec![
1022            make_line(".if foo", 1),
1023            make_line("    nop", 2),
1024            make_line(".endif", 3),
1025        ];
1026
1027        let errors = expand_macros(lines).unwrap_err();
1028        assert!(matches!(
1029            &errors[0].error,
1030            CompileError::InvalidIfCondition { value, .. } if value == "foo"
1031        ));
1032    }
1033
1034    #[test]
1035    fn test_stray_else() {
1036        let lines = vec![make_line("    nop", 1), make_line(".else", 2)];
1037
1038        let errors = expand_macros(lines).unwrap_err();
1039        assert!(matches!(&errors[0].error, CompileError::StrayElse { .. }));
1040        assert_eq!(errors[0].origin.as_ref().unwrap().line, 2);
1041    }
1042
1043    #[test]
1044    fn test_stray_endif() {
1045        let lines = vec![make_line(".endif", 1)];
1046
1047        let errors = expand_macros(lines).unwrap_err();
1048        assert!(matches!(&errors[0].error, CompileError::StrayEndif { .. }));
1049        assert_eq!(errors[0].origin.as_ref().unwrap().line, 1);
1050    }
1051
1052    #[test]
1053    fn test_duplicate_else() {
1054        let lines = vec![
1055            make_line(".if 1", 1),
1056            make_line("    nop", 2),
1057            make_line(".else", 3),
1058            make_line(".else", 4),
1059            make_line(".endif", 5),
1060        ];
1061
1062        let errors = expand_macros(lines).unwrap_err();
1063        assert!(matches!(
1064            &errors[0].error,
1065            CompileError::DuplicateElse { .. }
1066        ));
1067        assert_eq!(errors[0].origin.as_ref().unwrap().line, 4);
1068    }
1069
1070    #[test]
1071    fn test_rept_zero() {
1072        let lines = vec![
1073            make_line(".rept 0", 1),
1074            make_line("    nop", 2),
1075            make_line(".endr", 3),
1076        ];
1077
1078        let result = expand_and_collect(lines);
1079        assert!(result.is_empty());
1080    }
1081
1082    #[test]
1083    fn test_irp() {
1084        let lines = vec![
1085            make_line(".irp reg, r1, r2, r3", 1),
1086            make_line("    mov64 \\reg, 0", 2),
1087            make_line(".endr", 3),
1088        ];
1089
1090        let result = expand_and_collect(lines);
1091        assert_eq!(
1092            result,
1093            vec!["    mov64 r1, 0", "    mov64 r2, 0", "    mov64 r3, 0"]
1094        );
1095    }
1096
1097    #[test]
1098    fn test_nested_rept() {
1099        let lines = vec![
1100            make_line(".rept 2", 1),
1101            make_line("    .rept 3", 2),
1102            make_line("        mov64 r1, 0x1", 3),
1103            make_line("    .endr", 4),
1104            make_line(".endr", 5),
1105        ];
1106
1107        let result = expand_and_collect(lines);
1108        assert_eq!(result.len(), 6);
1109        assert!(result.iter().all(|s| s == "        mov64 r1, 0x1"));
1110    }
1111
1112    #[test]
1113    fn test_nested_irp() {
1114        let lines = vec![
1115            make_line(".irp reg, r1, r2", 1),
1116            make_line("    .irp val, 0x1, 0x2", 2),
1117            make_line("        mov64 \\reg, \\val", 3),
1118            make_line("    .endr", 4),
1119            make_line(".endr", 5),
1120        ];
1121
1122        let result = expand_and_collect(lines);
1123        assert_eq!(
1124            result,
1125            vec![
1126                "        mov64 r1, 0x1",
1127                "        mov64 r1, 0x2",
1128                "        mov64 r2, 0x1",
1129                "        mov64 r2, 0x2",
1130            ]
1131        );
1132    }
1133
1134    #[test]
1135    fn test_rept_inside_irp() {
1136        let lines = vec![
1137            make_line(".irp r, r1, r2", 1),
1138            make_line("    .rept 2", 2),
1139            make_line("        mov64 \\r, 0x1", 3),
1140            make_line("    .endr", 4),
1141            make_line(".endr", 5),
1142        ];
1143
1144        let result = expand_and_collect(lines);
1145        assert_eq!(
1146            result,
1147            vec![
1148                "        mov64 r1, 0x1",
1149                "        mov64 r1, 0x1",
1150                "        mov64 r2, 0x1",
1151                "        mov64 r2, 0x1",
1152            ]
1153        );
1154    }
1155
1156    #[test]
1157    fn test_irp_inside_rept() {
1158        let lines = vec![
1159            make_line(".rept 2", 1),
1160            make_line("    .irp r, r1, r2", 2),
1161            make_line("        mov64 \\r, 0x1", 3),
1162            make_line("    .endr", 4),
1163            make_line(".endr", 5),
1164        ];
1165
1166        let result = expand_and_collect(lines);
1167        assert_eq!(
1168            result,
1169            vec![
1170                "        mov64 r1, 0x1",
1171                "        mov64 r2, 0x1",
1172                "        mov64 r1, 0x1",
1173                "        mov64 r2, 0x1",
1174            ]
1175        );
1176    }
1177
1178    #[test]
1179    fn test_rept_count_from_macro_param() {
1180        let lines = vec![
1181            make_line(".macro TEST num", 1),
1182            make_line("    .rept \\num", 2),
1183            make_line("        mov64 r1, 0x123", 3),
1184            make_line("    .endr", 4),
1185            make_line(".endm", 5),
1186            make_line("TEST 3", 7),
1187        ];
1188
1189        let result = expand_and_collect(lines);
1190        assert_eq!(
1191            result,
1192            vec![
1193                "        mov64 r1, 0x123",
1194                "        mov64 r1, 0x123",
1195                "        mov64 r1, 0x123",
1196            ]
1197        );
1198    }
1199
1200    #[test]
1201    fn test_irp_values_from_macro_vararg() {
1202        let lines = vec![
1203            make_line(".macro LOAD regs:vararg", 1),
1204            make_line("    .irp r, \\regs", 2),
1205            make_line("        mov64 \\r, 0x1", 3),
1206            make_line("    .endr", 4),
1207            make_line(".endm", 5),
1208            make_line("LOAD r1, r2, r3", 7),
1209        ];
1210
1211        let result = expand_and_collect(lines);
1212        assert_eq!(
1213            result,
1214            vec![
1215                "        mov64 r1, 0x1",
1216                "        mov64 r2, 0x1",
1217                "        mov64 r3, 0x1",
1218            ]
1219        );
1220    }
1221
1222    #[test]
1223    fn test_unclosed_rept() {
1224        let lines = vec![make_line(".rept 3", 1), make_line("    nop", 2)];
1225
1226        let result = expand_macros(lines);
1227        assert!(result.is_err());
1228        let errors = result.unwrap_err();
1229        assert!(matches!(
1230            &errors[0].error,
1231            CompileError::UnclosedRept { .. }
1232        ));
1233        assert!(errors[0].origin.is_some());
1234    }
1235
1236    #[test]
1237    fn test_full_example_from_spec() {
1238        let lines = vec![
1239            make_line(".macro DEF_STR name, text", 1),
1240            make_line("\\name:", 2),
1241            make_line("    .ascii \\text", 3),
1242            make_line("\\name\\()_end:", 4),
1243            make_line(".endm", 5),
1244            make_line("", 6),
1245            make_line(".macro RETURN_ERR code, msg", 7),
1246            make_line("    lddw r0, \\code", 8),
1247            make_line("    lddw r1, \\msg", 9),
1248            make_line("    lddw r2, \\msg\\()_end - \\msg", 10),
1249            make_line("    call sol_log_", 11),
1250            make_line("    exit", 12),
1251            make_line(".endm", 13),
1252            make_line("", 14),
1253            make_line("DEF_STR e1, \"error\"", 15),
1254            make_line("", 16),
1255            make_line("RETURN_ERR 1, e1", 17),
1256        ];
1257
1258        let result = expand_and_collect(lines);
1259        assert_eq!(
1260            result,
1261            vec![
1262                "", // blank line between definitions
1263                "", // blank line after definitions
1264                "e1:",
1265                "    .ascii \"error\"",
1266                "e1_end:",
1267                "", // blank line
1268                "    lddw r0, 1",
1269                "    lddw r1, e1",
1270                "    lddw r2, e1_end - e1",
1271                "    call sol_log_",
1272                "    exit",
1273            ]
1274        );
1275    }
1276
1277    #[test]
1278    fn test_passthrough_no_macros() {
1279        let lines = vec![
1280            make_line(".globl entrypoint", 1),
1281            make_line("entrypoint:", 2),
1282            make_line("    mov64 r1, 42", 3),
1283            make_line("    exit", 4),
1284        ];
1285
1286        let result = expand_and_collect(lines);
1287        assert_eq!(
1288            result,
1289            vec![
1290                ".globl entrypoint",
1291                "entrypoint:",
1292                "    mov64 r1, 42",
1293                "    exit",
1294            ]
1295        );
1296    }
1297}