1use alloc::borrow::Cow;
8use alloc::string::String;
9#[allow(unused_imports)]
10use alloc::vec;
11use alloc::vec::Vec;
12use core::str;
13
14use crate::error::{AsmError, Span};
15use crate::ir::Syntax;
16
17#[derive(Debug, Clone, PartialEq)]
23pub struct Token<'src> {
24 pub kind: TokenKind,
26 pub text: Cow<'src, str>,
28 pub span: Span,
30}
31
32impl<'src> Token<'src> {
33 #[inline]
35 pub fn text(&self) -> &str {
36 &self.text
37 }
38}
39
40#[derive(Debug, Clone, PartialEq)]
42pub enum TokenKind {
43 Ident,
45 Number(i128),
47 StringLit,
49 CharLit(u8),
51 Directive,
53 LabelDef,
55 NumericLabelDef(u32),
57 NumericLabelFwd(u32),
59 NumericLabelBwd(u32),
61 Comma,
63 OpenBracket,
65 CloseBracket,
67 Plus,
69 Minus,
71 Star,
73 Colon,
75 Equals,
77 OpenBrace,
79 CloseBrace,
81 OpenParen,
83 CloseParen,
85 Bang,
87 Percent,
89 Dollar,
91 Slash,
93 Ampersand,
95 Pipe,
97 Caret,
99 Tilde,
101 LShift,
103 RShift,
105 Newline,
107 Eof,
109}
110
111pub fn tokenize(source: &str) -> Result<Vec<Token<'_>>, AsmError> {
123 tokenize_with_syntax(source, Syntax::Intel)
124}
125
126pub fn tokenize_with_syntax(source: &str, syntax: Syntax) -> Result<Vec<Token<'_>>, AsmError> {
154 let ual = syntax == Syntax::Ual;
155 const MAX_PREALLOC_TOKENS: usize = 64 * 1024;
159 let mut tokens = Vec::with_capacity(core::cmp::min(source.len() / 3 + 1, MAX_PREALLOC_TOKENS));
160 let bytes = source.as_bytes();
161 let len = bytes.len();
162 let mut pos = 0;
163 let mut line: u32 = 1;
164 let mut col: u32 = 1;
165 let mut line_start = 0usize;
166
167 while pos < len {
168 let ch = bytes[pos];
169
170 if ch == b' ' || ch == b'\t' || ch == b'\r' {
172 pos += 1;
173 col += 1;
174 continue;
175 }
176
177 if ch == b'\n' {
179 tokens.push(Token {
180 kind: TokenKind::Newline,
181 text: Cow::Borrowed("\n"),
182 span: Span::new(line, col, pos, 1),
183 });
184 pos += 1;
185 line += 1;
186 col = 1;
187 line_start = pos;
188 continue;
189 }
190
191 if ch == b';' {
193 let start = pos;
194 tokens.push(Token {
195 kind: TokenKind::Newline,
196 text: Cow::Borrowed(";"),
197 span: Span::new(line, col, start, 1),
198 });
199 pos += 1;
200 col += 1;
201 continue;
202 }
203
204 let starts_comment = match ch {
207 b'/' => pos + 1 < len && bytes[pos + 1] == b'/',
208 b'@' => ual,
209 b'#' => !ual,
210 _ => false,
211 };
212 if starts_comment {
213 while pos < len && bytes[pos] != b'\n' {
214 pos += 1;
215 }
216 col = (pos - line_start) as u32 + 1;
217 continue;
218 }
219
220 if ch == b'#' {
230 let next = bytes.get(pos + 1).copied();
231 let immediate_follows = next.is_some_and(|c| {
232 c.is_ascii_digit()
233 || c.is_ascii_alphabetic()
234 || matches!(c, b'_' | b'-' | b'+' | b'\'' | b'(' | b'~')
235 });
236 if !immediate_follows {
237 return Err(AsmError::Syntax {
238 msg: String::from("'#' must be followed by an immediate value"),
239 span: Span::new(line, col, pos, 1),
240 });
241 }
242 pos += 1;
243 col += 1;
244 continue;
245 }
246
247 if ch == b',' {
249 tokens.push(Token {
250 kind: TokenKind::Comma,
251 text: Cow::Borrowed(","),
252 span: Span::new(line, col, pos, 1),
253 });
254 pos += 1;
255 col += 1;
256 continue;
257 }
258
259 if ch == b'[' {
261 tokens.push(Token {
262 kind: TokenKind::OpenBracket,
263 text: Cow::Borrowed("["),
264 span: Span::new(line, col, pos, 1),
265 });
266 pos += 1;
267 col += 1;
268 continue;
269 }
270 if ch == b']' {
271 tokens.push(Token {
272 kind: TokenKind::CloseBracket,
273 text: Cow::Borrowed("]"),
274 span: Span::new(line, col, pos, 1),
275 });
276 pos += 1;
277 col += 1;
278 continue;
279 }
280
281 if ch == b'+' {
283 tokens.push(Token {
284 kind: TokenKind::Plus,
285 text: Cow::Borrowed("+"),
286 span: Span::new(line, col, pos, 1),
287 });
288 pos += 1;
289 col += 1;
290 continue;
291 }
292
293 if ch == b'-' {
295 let is_unary = tokens.is_empty()
297 || matches!(
298 tokens.last().map(|t| &t.kind),
299 Some(
300 TokenKind::Comma
301 | TokenKind::OpenBracket
302 | TokenKind::OpenBrace
303 | TokenKind::Plus
304 | TokenKind::Minus
305 | TokenKind::Star
306 | TokenKind::Newline
307 | TokenKind::Equals
308 )
309 );
310
311 if is_unary && pos + 1 < len && bytes[pos + 1].is_ascii_digit() {
312 let start = pos;
314 let start_col = col;
315 pos += 1; let value = parse_number_at(bytes, &mut pos, line, start_col)?;
317 let token_len = pos - start;
318 let text = Cow::Borrowed(str::from_utf8(&bytes[start..pos]).unwrap_or(""));
319 tokens.push(Token {
320 kind: TokenKind::Number(-value),
321 text,
322 span: Span::new(line, start_col, start, token_len),
323 });
324 col = (pos - line_start) as u32 + 1;
325 continue;
326 }
327
328 tokens.push(Token {
329 kind: TokenKind::Minus,
330 text: Cow::Borrowed("-"),
331 span: Span::new(line, col, pos, 1),
332 });
333 pos += 1;
334 col += 1;
335 continue;
336 }
337
338 if ch == b'*' {
340 tokens.push(Token {
341 kind: TokenKind::Star,
342 text: Cow::Borrowed("*"),
343 span: Span::new(line, col, pos, 1),
344 });
345 pos += 1;
346 col += 1;
347 continue;
348 }
349
350 if ch == b':' {
352 tokens.push(Token {
353 kind: TokenKind::Colon,
354 text: Cow::Borrowed(":"),
355 span: Span::new(line, col, pos, 1),
356 });
357 pos += 1;
358 col += 1;
359 continue;
360 }
361
362 if ch == b'=' {
364 tokens.push(Token {
365 kind: TokenKind::Equals,
366 text: Cow::Borrowed("="),
367 span: Span::new(line, col, pos, 1),
368 });
369 pos += 1;
370 col += 1;
371 continue;
372 }
373
374 if ch == b'"' {
376 let start = pos;
377 let start_col = col;
378 pos += 1;
379 col += 1;
380 let mut content = Vec::new();
381 while pos < len && bytes[pos] != b'"' {
382 if bytes[pos] == b'\\' && pos + 1 < len {
383 pos += 1;
384 col += 1;
385 match bytes[pos] {
386 b'n' => content.push(b'\n'),
387 b't' => content.push(b'\t'),
388 b'\\' => content.push(b'\\'),
389 b'"' => content.push(b'"'),
390 b'0' => content.push(0),
391 b'x' => {
392 if pos + 2 < len {
394 let hi = hex_digit(bytes[pos + 1]);
395 let lo = hex_digit(bytes[pos + 2]);
396 if let (Some(h), Some(l)) = (hi, lo) {
397 content.push(h * 16 + l);
398 pos += 2;
399 col += 2;
400 } else {
401 return Err(AsmError::Syntax {
402 msg: String::from("invalid \\xHH escape sequence"),
403 span: Span::new(line, col, pos, 3),
404 });
405 }
406 }
407 }
408 _ => {
409 return Err(AsmError::Syntax {
410 msg: alloc::format!(
411 "unknown escape sequence '\\{}'",
412 bytes[pos] as char
413 ),
414 span: Span::new(line, col, pos - 1, 2),
415 });
416 }
417 }
418 } else if bytes[pos] == b'\n' {
419 return Err(AsmError::Syntax {
420 msg: String::from("unterminated string literal"),
421 span: Span::new(line, start_col, start, pos - start),
422 });
423 } else {
424 content.push(bytes[pos]);
425 }
426 pos += 1;
427 col += 1;
428 }
429 if pos >= len {
430 return Err(AsmError::Syntax {
431 msg: String::from("unterminated string literal"),
432 span: Span::new(line, start_col, start, pos - start),
433 });
434 }
435 pos += 1; col += 1;
437 let text_str = Cow::Owned(String::from_utf8(content).unwrap_or_default());
438 tokens.push(Token {
439 kind: TokenKind::StringLit,
440 text: text_str,
441 span: Span::new(line, start_col, start, pos - start),
442 });
443 continue;
444 }
445
446 if ch == b'\'' {
448 let start = pos;
449 let start_col = col;
450 pos += 1;
451 col += 1;
452 if pos >= len {
453 return Err(AsmError::Syntax {
454 msg: String::from("unterminated character literal"),
455 span: Span::new(line, start_col, start, 1),
456 });
457 }
458 let ch_val = if bytes[pos] == b'\\' && pos + 1 < len {
459 pos += 1;
460 col += 1;
461 match bytes[pos] {
462 b'n' => b'\n',
463 b't' => b'\t',
464 b'\\' => b'\\',
465 b'\'' => b'\'',
466 b'0' => 0,
467 _ => {
468 return Err(AsmError::Syntax {
469 msg: "unknown escape in character literal".into(),
470 span: Span::new(line, col, pos - 1, 2),
471 });
472 }
473 }
474 } else {
475 bytes[pos]
476 };
477 pos += 1;
478 col += 1;
479 if pos >= len || bytes[pos] != b'\'' {
480 return Err(AsmError::Syntax {
481 msg: String::from("unterminated character literal"),
482 span: Span::new(line, start_col, start, pos - start),
483 });
484 }
485 pos += 1;
486 col += 1;
487 tokens.push(Token {
488 kind: TokenKind::CharLit(ch_val),
489 text: Cow::Owned(alloc::format!("'{}'", ch_val as char)),
490 span: Span::new(line, start_col, start, pos - start),
491 });
492 continue;
493 }
494
495 if ch == b'.' {
497 let start = pos;
498 let start_col = col;
499 pos += 1;
500 col += 1;
501 while pos < len && (bytes[pos].is_ascii_alphanumeric() || bytes[pos] == b'_') {
502 pos += 1;
503 col += 1;
504 }
505 let text = Cow::Borrowed(str::from_utf8(&bytes[start..pos]).unwrap_or(""));
506 tokens.push(Token {
507 kind: TokenKind::Directive,
508 text,
509 span: Span::new(line, start_col, start, pos - start),
510 });
511 continue;
512 }
513
514 if ch.is_ascii_digit() {
516 let start = pos;
517 let start_col = col;
518
519 let mut temp = pos;
522 while temp < len && bytes[temp].is_ascii_digit() {
523 temp += 1;
524 }
525 if temp < len && bytes[temp] == b':' && (temp + 1 >= len || bytes[temp + 1] != b':') {
530 let num_str = str::from_utf8(&bytes[start..temp]).unwrap_or("0");
532 if let Ok(n) = num_str.parse::<u32>() {
533 if temp != start + 1 {
534 return Err(AsmError::Syntax {
535 msg: alloc::format!(
536 "numeric labels must be a single digit (0-9), got `{}`",
537 n
538 ),
539 span: Span::new(line, start_col, start, temp - start + 1),
540 });
541 }
542 pos = temp + 1; col = (pos - line_start) as u32 + 1;
544 tokens.push(Token {
545 kind: TokenKind::NumericLabelDef(n),
546 text: Cow::Owned(alloc::format!("{}:", n)),
547 span: Span::new(line, start_col, start, pos - start),
548 });
549 continue;
550 }
551 }
552 if temp < len && temp == start + 1 && (bytes[temp] == b'b' || bytes[temp] == b'f') {
554 let digit = bytes[start] - b'0';
556 let suffix = bytes[temp];
557 if !(digit == 0
558 && suffix == b'b'
559 && temp + 1 < len
560 && (bytes[temp + 1] == b'0' || bytes[temp + 1] == b'1'))
561 {
562 pos = temp + 1;
563 col = (pos - line_start) as u32 + 1;
564 let kind = if suffix == b'b' {
565 TokenKind::NumericLabelBwd(digit as u32)
566 } else {
567 TokenKind::NumericLabelFwd(digit as u32)
568 };
569 tokens.push(Token {
570 kind,
571 text: Cow::Owned(alloc::format!("{}{}", digit, suffix as char)),
572 span: Span::new(line, start_col, start, pos - start),
573 });
574 continue;
575 }
576 }
577
578 let value = parse_number_at(bytes, &mut pos, line, start_col)?;
579 let token_len = pos - start;
580 let text = Cow::Borrowed(str::from_utf8(&bytes[start..pos]).unwrap_or(""));
581 tokens.push(Token {
582 kind: TokenKind::Number(value),
583 text,
584 span: Span::new(line, start_col, start, token_len),
585 });
586 col = (pos - line_start) as u32 + 1;
587 continue;
588 }
589
590 if ch.is_ascii_alphabetic() || ch == b'_' {
592 let start = pos;
593 let start_col = col;
594 while pos < len
595 && (bytes[pos].is_ascii_alphanumeric() || bytes[pos] == b'_' || bytes[pos] == b'.')
596 {
597 pos += 1;
598 }
599 let text = Cow::Borrowed(str::from_utf8(&bytes[start..pos]).unwrap_or(""));
600 let token_len = pos - start;
601
602 if pos < len && bytes[pos] == b':' {
605 let is_segment_reg = text.eq_ignore_ascii_case("cs")
606 || text.eq_ignore_ascii_case("ds")
607 || text.eq_ignore_ascii_case("es")
608 || text.eq_ignore_ascii_case("fs")
609 || text.eq_ignore_ascii_case("gs")
610 || text.eq_ignore_ascii_case("ss");
611 if is_segment_reg {
612 tokens.push(Token {
614 kind: TokenKind::Ident,
615 text,
616 span: Span::new(line, start_col, start, token_len),
617 });
618 col = (pos - line_start) as u32 + 1;
619 continue;
620 }
621 pos += 1; tokens.push(Token {
623 kind: TokenKind::LabelDef,
624 text,
625 span: Span::new(line, start_col, start, pos - start),
626 });
627 col = (pos - line_start) as u32 + 1;
628 continue;
629 }
630
631 tokens.push(Token {
632 kind: TokenKind::Ident,
633 text,
634 span: Span::new(line, start_col, start, token_len),
635 });
636 col = (pos - line_start) as u32 + 1;
637 continue;
638 }
639
640 if ch == b'{' {
642 tokens.push(Token {
643 kind: TokenKind::OpenBrace,
644 text: Cow::Borrowed("{"),
645 span: Span::new(line, col, pos, 1),
646 });
647 pos += 1;
648 col += 1;
649 continue;
650 }
651
652 if ch == b'}' {
654 tokens.push(Token {
655 kind: TokenKind::CloseBrace,
656 text: Cow::Borrowed("}"),
657 span: Span::new(line, col, pos, 1),
658 });
659 pos += 1;
660 col += 1;
661 continue;
662 }
663
664 if ch == b'(' {
666 tokens.push(Token {
667 kind: TokenKind::OpenParen,
668 text: Cow::Borrowed("("),
669 span: Span::new(line, col, pos, 1),
670 });
671 pos += 1;
672 col += 1;
673 continue;
674 }
675
676 if ch == b')' {
678 tokens.push(Token {
679 kind: TokenKind::CloseParen,
680 text: Cow::Borrowed(")"),
681 span: Span::new(line, col, pos, 1),
682 });
683 pos += 1;
684 col += 1;
685 continue;
686 }
687
688 if ch == b'!' {
690 tokens.push(Token {
691 kind: TokenKind::Bang,
692 text: Cow::Borrowed("!"),
693 span: Span::new(line, col, pos, 1),
694 });
695 pos += 1;
696 col += 1;
697 continue;
698 }
699
700 if ch == b'%' {
702 tokens.push(Token {
703 kind: TokenKind::Percent,
704 text: Cow::Borrowed("%"),
705 span: Span::new(line, col, pos, 1),
706 });
707 pos += 1;
708 col += 1;
709 continue;
710 }
711
712 if ch == b'$' {
714 tokens.push(Token {
715 kind: TokenKind::Dollar,
716 text: Cow::Borrowed("$"),
717 span: Span::new(line, col, pos, 1),
718 });
719 pos += 1;
720 col += 1;
721 continue;
722 }
723
724 if ch == b'/' {
726 if pos + 1 < len && bytes[pos + 1] == b'/' {
728 pos += 2;
730 while pos < len && bytes[pos] != b'\n' {
731 pos += 1;
732 }
733 col = (pos - line_start) as u32 + 1;
734 continue;
735 }
736 if pos + 1 < len && bytes[pos + 1] == b'*' {
737 let comment_start_line = line;
739 let comment_start_col = col;
740 let comment_start_pos = pos;
741 pos += 2;
742 col += 2;
743 while pos + 1 < len && !(bytes[pos] == b'*' && bytes[pos + 1] == b'/') {
744 if bytes[pos] == b'\n' {
745 line += 1;
746 col = 1;
747 line_start = pos + 1;
748 } else {
749 col += 1;
750 }
751 pos += 1;
752 }
753 if pos + 1 < len {
754 pos += 2; col += 2;
756 } else {
757 return Err(AsmError::Syntax {
759 msg: String::from("unterminated block comment"),
760 span: Span::new(
761 comment_start_line,
762 comment_start_col,
763 comment_start_pos,
764 2,
765 ),
766 });
767 }
768 continue;
769 }
770 tokens.push(Token {
771 kind: TokenKind::Slash,
772 text: Cow::Borrowed("/"),
773 span: Span::new(line, col, pos, 1),
774 });
775 pos += 1;
776 col += 1;
777 continue;
778 }
779
780 if ch == b'&' {
782 tokens.push(Token {
783 kind: TokenKind::Ampersand,
784 text: Cow::Borrowed("&"),
785 span: Span::new(line, col, pos, 1),
786 });
787 pos += 1;
788 col += 1;
789 continue;
790 }
791
792 if ch == b'|' {
794 tokens.push(Token {
795 kind: TokenKind::Pipe,
796 text: Cow::Borrowed("|"),
797 span: Span::new(line, col, pos, 1),
798 });
799 pos += 1;
800 col += 1;
801 continue;
802 }
803
804 if ch == b'^' {
806 tokens.push(Token {
807 kind: TokenKind::Caret,
808 text: Cow::Borrowed("^"),
809 span: Span::new(line, col, pos, 1),
810 });
811 pos += 1;
812 col += 1;
813 continue;
814 }
815
816 if ch == b'~' {
818 tokens.push(Token {
819 kind: TokenKind::Tilde,
820 text: Cow::Borrowed("~"),
821 span: Span::new(line, col, pos, 1),
822 });
823 pos += 1;
824 col += 1;
825 continue;
826 }
827
828 if ch == b'<' && pos + 1 < len && bytes[pos + 1] == b'<' {
830 tokens.push(Token {
831 kind: TokenKind::LShift,
832 text: Cow::Borrowed("<<"),
833 span: Span::new(line, col, pos, 2),
834 });
835 pos += 2;
836 col += 2;
837 continue;
838 }
839 if ch == b'>' && pos + 1 < len && bytes[pos + 1] == b'>' {
840 tokens.push(Token {
841 kind: TokenKind::RShift,
842 text: Cow::Borrowed(">>"),
843 span: Span::new(line, col, pos, 2),
844 });
845 pos += 2;
846 col += 2;
847 continue;
848 }
849
850 return Err(AsmError::Syntax {
852 msg: alloc::format!("unexpected character '{}'", ch as char),
853 span: Span::new(line, col, pos, 1),
854 });
855 }
856
857 tokens.push(Token {
858 kind: TokenKind::Eof,
859 text: Cow::Borrowed(""),
860 span: Span::new(line, col, pos, 0),
861 });
862
863 Ok(tokens)
864}
865
866#[inline]
868fn parse_number_at(
869 bytes: &[u8],
870 pos: &mut usize,
871 span_line: u32,
872 span_col: u32,
873) -> Result<i128, AsmError> {
874 let start = *pos;
875 let len = bytes.len();
876
877 if *pos >= len {
878 return Err(AsmError::Syntax {
879 msg: String::from("expected number"),
880 span: Span::new(span_line, span_col, start, 0),
881 });
882 }
883
884 if bytes[*pos] == b'0' && *pos + 1 < len {
886 match bytes[*pos + 1] {
887 b'x' | b'X' => {
888 *pos += 2;
889 let num_start = *pos;
890 while *pos < len && bytes[*pos].is_ascii_hexdigit() {
891 *pos += 1;
892 }
893 if *pos == num_start {
894 return Err(AsmError::Syntax {
895 msg: String::from("expected hex digits after '0x'"),
896 span: Span::new(span_line, span_col, start, *pos - start),
897 });
898 }
899 let s = str::from_utf8(&bytes[num_start..*pos]).unwrap_or("0");
900 return i128::from_str_radix(s, 16).map_err(|_| AsmError::Syntax {
901 msg: alloc::format!("invalid hex number '0x{}'", s),
902 span: Span::new(span_line, span_col, start, *pos - start),
903 });
904 }
905 b'b' | b'B' => {
906 if *pos + 2 < len && (bytes[*pos + 2] == b'0' || bytes[*pos + 2] == b'1') {
908 *pos += 2;
909 let num_start = *pos;
910 while *pos < len && (bytes[*pos] == b'0' || bytes[*pos] == b'1') {
911 *pos += 1;
912 }
913 let s = str::from_utf8(&bytes[num_start..*pos]).unwrap_or("0");
914 return i128::from_str_radix(s, 2).map_err(|_| AsmError::Syntax {
915 msg: alloc::format!("invalid binary number '0b{}'", s),
916 span: Span::new(span_line, span_col, start, *pos - start),
917 });
918 }
919 }
921 b'o' | b'O' => {
922 *pos += 2;
923 let num_start = *pos;
924 while *pos < len && bytes[*pos] >= b'0' && bytes[*pos] <= b'7' {
925 *pos += 1;
926 }
927 if *pos == num_start {
928 return Err(AsmError::Syntax {
929 msg: String::from("expected octal digits after '0o'"),
930 span: Span::new(span_line, span_col, start, *pos - start),
931 });
932 }
933 let s = str::from_utf8(&bytes[num_start..*pos]).unwrap_or("0");
934 return i128::from_str_radix(s, 8).map_err(|_| AsmError::Syntax {
935 msg: alloc::format!("invalid octal number '0o{}'", s),
936 span: Span::new(span_line, span_col, start, *pos - start),
937 });
938 }
939 _ => {}
940 }
941 }
942
943 while *pos < len && bytes[*pos].is_ascii_digit() {
945 *pos += 1;
946 }
947 if *pos < len && (bytes[*pos] == b'h' || bytes[*pos] == b'H') {
949 let s = str::from_utf8(&bytes[start..*pos]).unwrap_or("0");
950 *pos += 1; return i128::from_str_radix(s, 16).map_err(|_| AsmError::Syntax {
952 msg: alloc::format!("invalid hex number '{}h'", s),
953 span: Span::new(span_line, span_col, start, *pos - start),
954 });
955 }
956 let s = str::from_utf8(&bytes[start..*pos]).unwrap_or("0");
957 s.parse::<i128>().map_err(|_| AsmError::Syntax {
958 msg: alloc::format!("invalid number '{}'", s),
959 span: Span::new(span_line, span_col, start, *pos - start),
960 })
961}
962
963#[inline]
964fn hex_digit(b: u8) -> Option<u8> {
965 match b {
966 b'0'..=b'9' => Some(b - b'0'),
967 b'a'..=b'f' => Some(b - b'a' + 10),
968 b'A'..=b'F' => Some(b - b'A' + 10),
969 _ => None,
970 }
971}
972
973#[cfg(test)]
974mod tests {
975 use super::*;
976
977 fn tok_kinds(src: &str) -> Vec<TokenKind> {
978 tokenize(src).unwrap().into_iter().map(|t| t.kind).collect()
979 }
980
981 #[allow(dead_code)]
982 fn tok_texts(src: &str) -> Vec<String> {
983 tokenize(src)
984 .unwrap()
985 .into_iter()
986 .map(|t| t.text.into_owned())
987 .collect()
988 }
989
990 #[test]
991 fn empty_input() {
992 let tokens = tokenize("").unwrap();
993 assert_eq!(tokens.len(), 1);
994 assert_eq!(tokens[0].kind, TokenKind::Eof);
995 }
996
997 #[test]
998 fn only_whitespace() {
999 let tokens = tokenize(" \t ").unwrap();
1000 assert_eq!(tokens.len(), 1);
1001 assert_eq!(tokens[0].kind, TokenKind::Eof);
1002 }
1003
1004 #[test]
1005 fn only_comment() {
1006 let tokens = tokenize("# this is a comment").unwrap();
1008 assert_eq!(tokens.len(), 1);
1009 assert_eq!(tokens[0].kind, TokenKind::Eof);
1010 }
1011
1012 #[test]
1013 fn hash_comment() {
1014 let tokens = tokenize("# comment").unwrap();
1015 assert_eq!(tokens.len(), 1);
1016 assert_eq!(tokens[0].kind, TokenKind::Eof);
1017 }
1018
1019 #[test]
1020 fn simple_instruction() {
1021 let kinds = tok_kinds("mov rax, rbx");
1022 assert_eq!(
1023 kinds,
1024 vec![
1025 TokenKind::Ident, TokenKind::Ident, TokenKind::Comma,
1028 TokenKind::Ident, TokenKind::Eof,
1030 ]
1031 );
1032 }
1033
1034 #[test]
1035 fn instruction_with_immediate() {
1036 let tokens = tokenize("mov rax, 42").unwrap();
1037 assert_eq!(tokens[3].kind, TokenKind::Number(42));
1038 }
1039
1040 #[test]
1041 fn hex_immediate() {
1042 let tokens = tokenize("mov rax, 0xFF").unwrap();
1043 assert_eq!(tokens[3].kind, TokenKind::Number(255));
1044 }
1045
1046 #[test]
1047 fn hex_uppercase() {
1048 let tokens = tokenize("mov rax, 0XAB").unwrap();
1049 assert_eq!(tokens[3].kind, TokenKind::Number(0xAB));
1050 }
1051
1052 #[test]
1053 fn binary_immediate() {
1054 let tokens = tokenize("mov rax, 0b1010").unwrap();
1055 assert_eq!(tokens[3].kind, TokenKind::Number(10));
1056 }
1057
1058 #[test]
1059 fn octal_immediate() {
1060 let tokens = tokenize("mov rax, 0o77").unwrap();
1061 assert_eq!(tokens[3].kind, TokenKind::Number(63));
1062 }
1063
1064 #[test]
1065 fn negative_immediate() {
1066 let tokens = tokenize("mov rax, -1").unwrap();
1067 assert_eq!(tokens[3].kind, TokenKind::Number(-1));
1068 }
1069
1070 #[test]
1071 fn negative_hex() {
1072 let tokens = tokenize("add rsp, -0x10").unwrap();
1073 assert_eq!(tokens[3].kind, TokenKind::Number(-16));
1074 }
1075
1076 #[test]
1077 fn label_definition() {
1078 let tokens = tokenize("entry_point:").unwrap();
1079 assert_eq!(tokens[0].kind, TokenKind::LabelDef);
1080 assert_eq!(tokens[0].text, "entry_point");
1081 }
1082
1083 #[test]
1084 fn label_definition_with_instruction() {
1085 let kinds = tok_kinds("loop: dec rcx");
1086 assert_eq!(kinds[0], TokenKind::LabelDef);
1087 assert_eq!(kinds[1], TokenKind::Ident); assert_eq!(kinds[2], TokenKind::Ident); }
1090
1091 #[test]
1092 fn numeric_label_def() {
1093 let tokens = tokenize("1:").unwrap();
1094 assert_eq!(tokens[0].kind, TokenKind::NumericLabelDef(1));
1095 }
1096
1097 #[test]
1098 fn numeric_label_backward_ref() {
1099 let tokens = tokenize("jnz 1b").unwrap();
1100 assert_eq!(tokens[1].kind, TokenKind::NumericLabelBwd(1));
1101 }
1102
1103 #[test]
1104 fn numeric_label_forward_ref() {
1105 let tokens = tokenize("jmp 2f").unwrap();
1106 assert_eq!(tokens[1].kind, TokenKind::NumericLabelFwd(2));
1107 }
1108
1109 #[test]
1110 fn directive() {
1111 let tokens = tokenize(".byte 0x90").unwrap();
1112 assert_eq!(tokens[0].kind, TokenKind::Directive);
1113 assert_eq!(tokens[0].text, ".byte");
1114 assert_eq!(tokens[1].kind, TokenKind::Number(0x90));
1115 }
1116
1117 #[test]
1118 fn equ_directive() {
1119 let tokens = tokenize(".equ SYS_WRITE, 1").unwrap();
1120 assert_eq!(tokens[0].kind, TokenKind::Directive);
1121 assert_eq!(tokens[0].text, ".equ");
1122 assert_eq!(tokens[1].kind, TokenKind::Ident);
1123 assert_eq!(tokens[1].text, "SYS_WRITE");
1124 }
1125
1126 #[test]
1127 fn memory_operand_tokens() {
1128 let kinds = tok_kinds("[rax + rbx*4 + 8]");
1129 assert_eq!(
1130 kinds,
1131 vec![
1132 TokenKind::OpenBracket,
1133 TokenKind::Ident, TokenKind::Plus,
1135 TokenKind::Ident, TokenKind::Star,
1137 TokenKind::Number(4),
1138 TokenKind::Plus,
1139 TokenKind::Number(8),
1140 TokenKind::CloseBracket,
1141 TokenKind::Eof,
1142 ]
1143 );
1144 }
1145
1146 #[test]
1147 fn string_literal() {
1148 let tokens = tokenize(".asciz \"hello\"").unwrap();
1149 assert_eq!(tokens[1].kind, TokenKind::StringLit);
1150 assert_eq!(tokens[1].text, "hello");
1151 }
1152
1153 #[test]
1154 fn string_escape_sequences() {
1155 let tokens = tokenize(".ascii \"a\\nb\\t\\\\c\\0\\x41\"").unwrap();
1156 assert_eq!(tokens[1].kind, TokenKind::StringLit);
1157 assert_eq!(tokens[1].text, "a\nb\t\\c\0A");
1158 }
1159
1160 #[test]
1161 fn character_literal() {
1162 let tokens = tokenize("mov al, 'A'").unwrap();
1163 assert_eq!(tokens[3].kind, TokenKind::CharLit(b'A'));
1164 }
1165
1166 #[test]
1167 fn semicolon_separator() {
1168 let kinds = tok_kinds("nop; ret");
1169 assert_eq!(
1170 kinds,
1171 vec![
1172 TokenKind::Ident, TokenKind::Newline, TokenKind::Ident, TokenKind::Eof,
1176 ]
1177 );
1178 }
1179
1180 #[test]
1181 fn newline_separator() {
1182 let kinds = tok_kinds("nop\nret");
1183 assert_eq!(
1184 kinds,
1185 vec![
1186 TokenKind::Ident, TokenKind::Newline,
1188 TokenKind::Ident, TokenKind::Eof,
1190 ]
1191 );
1192 }
1193
1194 #[test]
1195 fn segment_override_tokens() {
1196 let kinds = tok_kinds("fs:[rax]");
1197 assert_eq!(kinds[0], TokenKind::Ident); assert_eq!(kinds[1], TokenKind::Colon);
1199 assert_eq!(kinds[2], TokenKind::OpenBracket);
1200 assert_eq!(kinds[3], TokenKind::Ident); assert_eq!(kinds[4], TokenKind::CloseBracket);
1202 }
1203
1204 #[test]
1205 fn size_hint_tokens() {
1206 let kinds = tok_kinds("byte ptr [rax]");
1207 assert_eq!(kinds[0], TokenKind::Ident); assert_eq!(kinds[1], TokenKind::Ident); assert_eq!(kinds[2], TokenKind::OpenBracket);
1210 }
1211
1212 #[test]
1213 fn prefix_and_instruction() {
1214 let kinds = tok_kinds("lock add [rax], 1");
1215 assert_eq!(kinds[0], TokenKind::Ident); assert_eq!(kinds[1], TokenKind::Ident); }
1218
1219 #[test]
1220 fn span_tracking() {
1221 let tokens = tokenize("mov rax, 1").unwrap();
1222 assert_eq!(tokens[0].span, Span::new(1, 1, 0, 3)); assert_eq!(tokens[1].span, Span::new(1, 5, 4, 3)); assert_eq!(tokens[2].span, Span::new(1, 8, 7, 1)); }
1226
1227 #[test]
1228 fn multiline_span_tracking() {
1229 let tokens = tokenize("nop\nmov rax, 1").unwrap();
1230 assert_eq!(tokens[0].span.line, 1); assert_eq!(tokens[2].span.line, 2); }
1233
1234 #[test]
1235 fn unknown_character_error() {
1236 let err = tokenize("mov rax, @").unwrap_err();
1237 match err {
1238 AsmError::Syntax { msg, .. } => {
1239 assert!(msg.contains("unexpected character '@'"));
1240 }
1241 _ => panic!("expected Syntax error"),
1242 }
1243 }
1244
1245 #[test]
1246 fn unterminated_string() {
1247 let err = tokenize(".ascii \"hello").unwrap_err();
1248 match err {
1249 AsmError::Syntax { msg, .. } => {
1250 assert!(msg.contains("unterminated string"));
1251 }
1252 _ => panic!("expected Syntax error"),
1253 }
1254 }
1255
1256 #[test]
1257 fn unterminated_block_comment() {
1258 let err = tokenize("nop /* this is never closed").unwrap_err();
1259 match err {
1260 AsmError::Syntax { msg, span } => {
1261 assert!(
1262 msg.contains("unterminated block comment"),
1263 "expected 'unterminated block comment', got: {msg}"
1264 );
1265 assert!(span.line > 0 || span.col > 0, "span should not be (0,0)");
1267 }
1268 _ => panic!("expected Syntax error"),
1269 }
1270 }
1271
1272 #[test]
1273 fn complex_instruction() {
1274 let tokens = tokenize("mov qword ptr [rbp - 0x10], rax").unwrap();
1275 let texts: Vec<_> = tokens.iter().map(|t| &*t.text).collect();
1276 assert_eq!(
1277 texts,
1278 vec!["mov", "qword", "ptr", "[", "rbp", "-", "0x10", "]", ",", "rax", ""]
1279 );
1280 }
1281
1282 #[test]
1283 fn all_punctuation() {
1284 let kinds = tok_kinds(", [ ] + - * :");
1285 assert_eq!(
1286 kinds,
1287 vec![
1288 TokenKind::Comma,
1289 TokenKind::OpenBracket,
1290 TokenKind::CloseBracket,
1291 TokenKind::Plus,
1292 TokenKind::Minus,
1293 TokenKind::Star,
1294 TokenKind::Colon,
1295 TokenKind::Eof,
1296 ]
1297 );
1298 }
1299
1300 #[test]
1301 fn trailing_whitespace() {
1302 let tokens = tokenize("nop ").unwrap();
1303 assert_eq!(tokens.len(), 2); }
1305
1306 #[test]
1307 fn zero_immediate() {
1308 let tokens = tokenize("xor eax, 0").unwrap();
1309 assert_eq!(tokens[3].kind, TokenKind::Number(0));
1310 }
1311
1312 #[test]
1313 fn large_hex_immediate() {
1314 let tokens = tokenize("mov rdi, 0x68732f2f6e69622f").unwrap();
1315 assert_eq!(tokens[3].kind, TokenKind::Number(0x68732f2f6e69622f));
1316 }
1317
1318 #[test]
1319 fn minus_in_memory_operand_is_not_unary() {
1320 let kinds = tok_kinds("[rbp - 0x10]");
1322 assert_eq!(
1323 kinds,
1324 vec![
1325 TokenKind::OpenBracket,
1326 TokenKind::Ident, TokenKind::Minus,
1328 TokenKind::Number(0x10),
1329 TokenKind::CloseBracket,
1330 TokenKind::Eof,
1331 ]
1332 );
1333 }
1334
1335 #[test]
1336 fn equals_token() {
1337 let kinds = tok_kinds("EXIT = 60");
1338 assert_eq!(
1339 kinds,
1340 vec![
1341 TokenKind::Ident, TokenKind::Equals,
1343 TokenKind::Number(60),
1344 TokenKind::Eof,
1345 ]
1346 );
1347 }
1348
1349 #[test]
1350 fn equals_with_negative() {
1351 let kinds = tok_kinds("NEG = -1");
1352 assert_eq!(
1353 kinds,
1354 vec![
1355 TokenKind::Ident,
1356 TokenKind::Equals,
1357 TokenKind::Number(-1),
1358 TokenKind::Eof,
1359 ]
1360 );
1361 }
1362}