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
16static EXPANSION_COUNTER: AtomicU64 = AtomicU64::new(0);
18
19#[derive(Debug)]
21pub(crate) struct ExpandError {
22 pub error: CompileError,
23 pub origin: Option<SourceOrigin>,
24}
25
26pub(crate) fn expand_macros(
30 lines: Vec<SourceLine>,
31) -> Result<(Vec<SourceLine>, Vec<ExpandError>), Vec<ExpandError>> {
32 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 for line in scan_result.remaining_lines {
52 expand_line(&line, ¯os, &mut output, &mut errors, 0);
53 }
54
55 let output = expand_repetitions(output)?;
57
58 Ok((output, errors))
59}
60
61fn 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 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 for body_line in ¯o_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 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 output.push(line.clone());
119 }
120}
121
122fn 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 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 if token.ends_with(':') {
142 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
153fn 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; 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
207fn 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 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 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 }
254
255 for (i, arg) in args.iter().enumerate() {
257 bindings.insert((i + 1).to_string(), arg.clone());
258 }
259
260 Ok(bindings)
261}
262
263fn 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 result.push('\\');
283 i += 2;
284 } else if next == '@' {
285 result.push_str(&expansion_id.to_string());
287 i += 2;
288 } else if next == '(' && i + 2 < len && chars[i + 2] == ')' {
289 i += 3;
291 } else if next.is_alphanumeric() || next == '_' {
292 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(¶m_name) {
301 result.push_str(value);
302 if end + 2 < len
304 && chars[end] == '\\'
305 && chars[end + 1] == '('
306 && chars[end + 2] == ')'
307 {
308 end += 3;
310 }
311 } else {
312 result.push('\\');
314 result.push_str(¶m_name);
315 }
316 i = end;
317 } else {
318 result.push('\\');
320 i += 1;
321 }
322 } else {
323 result.push(chars[i]);
324 i += 1;
325 }
326 }
327
328 result
329}
330
331fn 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
339fn 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 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 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 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 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
453fn eval_rept_count(s: &str) -> Option<usize> {
457 usize::try_from(eval_const(s)?).ok()
458}
459
460fn 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
484fn 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
507fn 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
526fn eval_const(s: &str) -> Option<i64> {
528 let (value, rest) = eval_const_expr(s)?;
529 rest.trim().is_empty().then_some(value)
530}
531
532fn is_if_directive(trimmed: &str) -> bool {
534 directive_arg(trimmed, ".if").is_some()
535}
536
537#[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
583fn 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); 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 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); 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 "", "", "e1:",
1265 " .ascii \"error\"",
1266 "e1_end:",
1267 "", " 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}