1use crate::{Format, IMM_MAX, Imm, Instruction, Opcode, REG_MAX, Reg, TABLE};
78use std::collections::BTreeMap;
79use std::fmt;
80
81#[derive(Clone, PartialEq, Eq, Debug)]
83pub struct AsmError {
84 pub line: usize,
86 pub kind: AsmErrorKind,
88}
89
90#[derive(Clone, PartialEq, Eq, Debug)]
92pub enum AsmErrorKind {
93 UnknownMnemonic(String),
95 WrongOperandCount {
97 mnemonic: &'static str,
99 expected: usize,
101 got: usize,
103 },
104 BadRegister(String),
106 BadImmediate(String),
108 UndefinedLabel(String),
110 DuplicateLabel(String),
112 Br3NotMirrored {
114 neg: i64,
116 reachable: i64,
118 pos: i64,
120 },
121 DuplicateEquate(String),
124 BadEquate(String),
126 ImmediateOutOfRange {
128 value: i64,
130 },
131}
132
133impl fmt::Display for AsmError {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 write!(f, "line {}: ", self.line)?;
136 match &self.kind {
137 AsmErrorKind::UnknownMnemonic(m) => write!(f, "unknown instruction {m:?}"),
138 AsmErrorKind::WrongOperandCount {
139 mnemonic,
140 expected,
141 got,
142 } => {
143 write!(f, "{mnemonic} takes {expected} operand(s), found {got}")
144 }
145 AsmErrorKind::BadRegister(t) => {
146 write!(f, "expected a register like r0 or r-13, found {t:?}")
147 }
148 AsmErrorKind::BadImmediate(t) => write!(f, "expected a number or label, found {t:?}"),
149 AsmErrorKind::UndefinedLabel(l) => write!(f, "undefined label {l:?}"),
150 AsmErrorKind::DuplicateLabel(l) => write!(f, "label {l:?} defined twice"),
151 AsmErrorKind::DuplicateEquate(n) => write!(f, "name {n:?} defined twice"),
152 AsmErrorKind::BadEquate(t) => {
153 write!(f, "expected `.equ NAME, VALUE`, found {t:?}")
154 }
155 AsmErrorKind::Br3NotMirrored {
156 neg,
157 reachable,
158 pos,
159 } => write!(
160 f,
161 "br3 targets must be mirrored about the branch: with the \
162 positive target at instruction {pos}, the negative branch \
163 reaches {reachable}, not {neg}. Use brn and brp for targets \
164 that cannot be mirrored."
165 ),
166 AsmErrorKind::ImmediateOutOfRange { value } => {
167 write!(f, "immediate {value} does not fit 12 trits (+/-{IMM_MAX})")
168 }
169 }
170 }
171}
172
173impl std::error::Error for AsmError {}
174
175#[derive(Clone, PartialEq, Eq, Debug, Default)]
177pub struct Program {
178 pub instructions: Vec<Instruction>,
180 pub labels: BTreeMap<String, usize>,
182 pub equates: BTreeMap<String, i64>,
184}
185
186const fn operand_count(f: Format) -> usize {
188 match f {
189 Format::R => 3,
190 Format::I => 3,
191 Format::U => 2,
192 Format::B => 2,
193 Format::J => 2,
194 Format::N => 0,
195 }
196}
197
198fn clean(line: &str) -> &str {
200 let cut = line.find([';', '#']).unwrap_or(line.len());
201 line[..cut].trim()
202}
203
204fn operands(s: &str) -> Vec<&str> {
206 s.split([',', ' ', '\t'])
207 .map(str::trim)
208 .filter(|t| !t.is_empty())
209 .collect()
210}
211
212fn parse_reg(tok: &str, line: usize) -> Result<Reg, AsmError> {
213 let err = || AsmError {
214 line,
215 kind: AsmErrorKind::BadRegister(tok.to_string()),
216 };
217 let rest = tok
218 .strip_prefix('r')
219 .or_else(|| tok.strip_prefix('R'))
220 .ok_or_else(err)?;
221 let n: i64 = rest.parse().map_err(|_| err())?;
222 if n < -(REG_MAX as i64) || n > REG_MAX as i64 {
223 return Err(err());
224 }
225 Reg::new(n as i8).ok_or_else(err)
226}
227
228fn split_displacement(tok: &str, line: usize) -> Result<(&str, i64), AsmError> {
234 let cut = tok
236 .rfind(['+', '-'])
237 .filter(|i| *i > 0);
239 let Some(cut) = cut else {
240 return Ok((tok, 0));
241 };
242 let (name, rest) = tok.split_at(cut);
243 let sign = if rest.starts_with('-') { -1 } else { 1 };
244 let magnitude: i64 = rest[1..].parse().map_err(|_| AsmError {
245 line,
246 kind: AsmErrorKind::BadImmediate(tok.to_string()),
247 })?;
248 Ok((name, sign * magnitude))
249}
250
251fn equate_body(text: &str) -> Option<&str> {
253 let rest = text
254 .strip_prefix(".equ")
255 .or_else(|| text.strip_prefix(".EQU"))?;
256 if rest.is_empty() || rest.starts_with([' ', '\t']) {
258 Some(rest.trim())
259 } else {
260 None
261 }
262}
263
264fn parse_equate(body: &str, line: usize) -> Result<(String, i64), AsmError> {
266 let err = || AsmError {
267 line,
268 kind: AsmErrorKind::BadEquate(body.to_string()),
269 };
270 let toks = operands(body);
271 if toks.len() != 2 {
272 return Err(err());
273 }
274 let name = toks[0];
275 if !name.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_')
276 || !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
277 {
278 return Err(err());
279 }
280 let value: i64 = toks[1].parse().map_err(|_| err())?;
281 Ok((name.to_string(), value))
282}
283
284fn lookup(mnemonic: &str) -> Option<Opcode> {
285 TABLE
286 .iter()
287 .find(|e| e.mnemonic.eq_ignore_ascii_case(mnemonic))
288 .map(|e| e.opcode)
289}
290
291pub fn assemble(source: &str) -> Result<Program, AsmError> {
297 assemble_at(source, 0)
298}
299
300pub fn assemble_at(source: &str, base: i64) -> Result<Program, AsmError> {
305 let mut labels: BTreeMap<String, usize> = BTreeMap::new();
308 let mut equates: BTreeMap<String, i64> = BTreeMap::new();
309 let mut index = 0usize;
310 for (n, raw) in source.lines().enumerate() {
311 let line = n + 1;
312 let mut text = clean(raw);
313 if let Some(rest) = equate_body(text) {
314 let (name, value) = parse_equate(rest, line)?;
315 if equates.insert(name.clone(), value).is_some() {
316 return Err(AsmError {
317 line,
318 kind: AsmErrorKind::DuplicateEquate(name),
319 });
320 }
321 continue;
322 }
323 while let Some(colon) = text.find(':') {
325 let name = text[..colon].trim().to_string();
326 if labels.insert(name.clone(), index).is_some() {
327 return Err(AsmError {
328 line,
329 kind: AsmErrorKind::DuplicateLabel(name),
330 });
331 }
332 text = text[colon + 1..].trim();
333 }
334 if !text.is_empty() {
335 index += 1;
336 }
337 }
338
339 if let Some(name) = labels.keys().find(|k| equates.contains_key(*k)) {
341 return Err(AsmError {
342 line: 0,
343 kind: AsmErrorKind::DuplicateEquate(name.clone()),
344 });
345 }
346
347 let mut program = Program {
349 instructions: Vec::new(),
350 labels,
351 equates,
352 };
353 let mut index = 0usize;
354 for (n, raw) in source.lines().enumerate() {
355 let line = n + 1;
356 let mut text = clean(raw);
357 if equate_body(text).is_some() {
358 continue;
359 }
360 while let Some(colon) = text.find(':') {
361 text = text[colon + 1..].trim();
362 }
363 if text.is_empty() {
364 continue;
365 }
366
367 let (mnemonic, rest) = match text.find([' ', '\t']) {
368 Some(i) => (&text[..i], &text[i..]),
369 None => (text, ""),
370 };
371 let op = lookup(mnemonic).ok_or_else(|| AsmError {
372 line,
373 kind: AsmErrorKind::UnknownMnemonic(mnemonic.to_string()),
374 })?;
375 let fmt = op.format();
376 let toks = operands(rest);
377 let want = operand_count(fmt);
378 let br3_pair = op == Opcode::Br3 && toks.len() == 3;
379 if toks.len() != want && !br3_pair {
380 return Err(AsmError {
381 line,
382 kind: AsmErrorKind::WrongOperandCount {
383 mnemonic: op.mnemonic(),
384 expected: want,
385 got: toks.len(),
386 },
387 });
388 }
389
390 let imm_of = |tok: &str| -> Result<Imm, AsmError> {
393 let (tok, displacement) = split_displacement(tok, line)?;
397 let value: i64 = if let Ok(v) = tok.parse::<i64>() {
398 v
399 } else if let Some(name) = tok.strip_prefix('@') {
400 let &target = program.labels.get(name).ok_or(AsmError {
404 line,
405 kind: AsmErrorKind::UndefinedLabel(name.to_string()),
406 })?;
407 base + target as i64 * crate::WORD_TRYTES
408 } else if let Some(&v) = program.equates.get(tok) {
409 v
411 } else if let Some(&target) = program.labels.get(tok) {
412 target as i64 - index as i64
413 } else if tok.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_') {
414 return Err(AsmError {
415 line,
416 kind: AsmErrorKind::UndefinedLabel(tok.to_string()),
417 });
418 } else {
419 return Err(AsmError {
420 line,
421 kind: AsmErrorKind::BadImmediate(tok.to_string()),
422 });
423 };
424 let value = value + displacement;
425 Imm::new(value as i32)
426 .filter(|_| value.abs() <= IMM_MAX as i64)
427 .ok_or(AsmError {
428 line,
429 kind: AsmErrorKind::ImmediateOutOfRange { value },
430 })
431 };
432
433 if br3_pair {
437 let rs1 = parse_reg(toks[0], line)?;
438 let resolve = |tok: &str| -> Result<i64, AsmError> {
439 program.labels.get(tok).map(|i| *i as i64).ok_or(AsmError {
440 line,
441 kind: AsmErrorKind::UndefinedLabel(tok.to_string()),
442 })
443 };
444 let neg = resolve(toks[1])?;
445 let pos = resolve(toks[2])?;
446 let here = index as i64;
447 let reachable = 2 * here - pos;
448 if neg != reachable {
449 return Err(AsmError {
450 line,
451 kind: AsmErrorKind::Br3NotMirrored {
452 neg,
453 reachable,
454 pos,
455 },
456 });
457 }
458 let offset = pos - here;
459 let imm = Imm::new(offset as i32)
460 .filter(|_| offset.abs() <= IMM_MAX as i64)
461 .ok_or(AsmError {
462 line,
463 kind: AsmErrorKind::ImmediateOutOfRange { value: offset },
464 })?;
465 program.instructions.push(Instruction::b(op, rs1, imm));
466 index += 1;
467 continue;
468 }
469
470 let instr = match fmt {
471 Format::R => Instruction::r(
472 op,
473 parse_reg(toks[0], line)?,
474 parse_reg(toks[1], line)?,
475 parse_reg(toks[2], line)?,
476 ),
477 Format::I => Instruction::i(
478 op,
479 parse_reg(toks[0], line)?,
480 parse_reg(toks[1], line)?,
481 imm_of(toks[2])?,
482 ),
483 Format::U => Instruction::u(op, parse_reg(toks[0], line)?, parse_reg(toks[1], line)?),
484 Format::B => Instruction::b(op, parse_reg(toks[0], line)?, imm_of(toks[1])?),
485 Format::J => Instruction::j(op, parse_reg(toks[0], line)?, imm_of(toks[1])?),
486 Format::N => Instruction::n(op),
487 };
488
489 program.instructions.push(instr);
490 index += 1;
491 }
492
493 Ok(program)
494}
495
496pub fn disassemble(instructions: &[Instruction]) -> String {
498 instructions
499 .iter()
500 .enumerate()
501 .map(|(i, ins)| format!("{i:>4} {ins}"))
502 .collect::<Vec<_>>()
503 .join("\n")
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509
510 #[test]
511 fn assembles_every_format() {
512 let src = "
513 nop
514 add r1, r2, r3
515 addi r4, r0, -100
516 neg r5, r6
517 br3 r7, 4
518 jal r8, -2
519 halt
520 ";
521 let p = assemble(src).unwrap();
522 assert_eq!(p.instructions.len(), 7);
523 assert_eq!(p.instructions[1].to_string(), "add r1, r2, r3");
524 assert_eq!(p.instructions[2].to_string(), "addi r4, r0, -100");
525 assert_eq!(p.instructions[4].to_string(), "br3 r7, 4");
526 }
527
528 #[test]
529 fn labels_resolve_to_relative_instruction_offsets() {
530 let src = "
531 top:
532 addi r1, r0, 3
533 loop:
534 addi r1, r1, -1
535 br3 r1, loop
536 jal r0, top
537 halt
538 ";
539 let p = assemble(src).unwrap();
540 assert_eq!(p.labels["top"], 0);
541 assert_eq!(p.labels["loop"], 1);
542 assert_eq!(p.instructions[2].imm.value(), -1);
544 assert_eq!(p.instructions[3].imm.value(), -3);
546 }
547
548 #[test]
549 fn a_label_may_share_a_line_with_an_instruction() {
550 let p = assemble("loop: addi r1, r1, -1\n br3 r1, loop").unwrap();
551 assert_eq!(p.labels["loop"], 0);
552 assert_eq!(p.instructions.len(), 2);
553 assert_eq!(p.instructions[1].imm.value(), -1);
554 }
555
556 #[test]
557 fn comments_and_blank_lines_are_ignored() {
558 let p = assemble("; leading\n\n nop # trailing\n\n; done\n").unwrap();
559 assert_eq!(p.instructions.len(), 1);
560 }
561
562 #[test]
563 fn round_trips_through_the_disassembler() {
564 let src = "add r1, r-2, r13\naddi r0, r5, 265720\nneg r7, r-13\nhalt";
565 let p = assemble(src).unwrap();
566
567 for instr in &p.instructions {
572 let text = instr.to_string();
573 let again = assemble(&text).unwrap();
574 assert_eq!(again.instructions.len(), 1, "{text}");
575 assert_eq!(&again.instructions[0], instr, "{text}");
576 }
577
578 assert_eq!(
580 disassemble(&p.instructions).lines().count(),
581 p.instructions.len()
582 );
583 }
584
585 #[test]
586 fn errors_carry_line_numbers() {
587 let e = assemble("nop\nfrobnicate r1, r2, r3").unwrap_err();
588 assert_eq!(e.line, 2);
589 assert!(matches!(e.kind, AsmErrorKind::UnknownMnemonic(_)));
590
591 let e = assemble("add r1, r2").unwrap_err();
592 assert!(matches!(
593 e.kind,
594 AsmErrorKind::WrongOperandCount {
595 expected: 3,
596 got: 2,
597 ..
598 }
599 ));
600
601 let e = assemble("add r1, r2, x9").unwrap_err();
602 assert!(matches!(e.kind, AsmErrorKind::BadRegister(_)));
603
604 let e = assemble("br3 r1, nowhere").unwrap_err();
605 assert!(matches!(e.kind, AsmErrorKind::UndefinedLabel(_)));
606
607 let e = assemble("a: nop\na: nop").unwrap_err();
608 assert!(matches!(e.kind, AsmErrorKind::DuplicateLabel(_)));
609
610 let e = assemble("addi r1, r0, 300000").unwrap_err();
611 assert!(matches!(e.kind, AsmErrorKind::ImmediateOutOfRange { .. }));
612
613 let e = assemble("add r1, r2, r99").unwrap_err();
614 assert!(matches!(e.kind, AsmErrorKind::BadRegister(_)));
615 }
616
617 #[test]
620 fn every_table_entry_is_reachable_by_mnemonic() {
621 for entry in TABLE {
622 assert_eq!(lookup(entry.mnemonic), Some(entry.opcode));
623 }
624 }
625}
626
627#[cfg(test)]
628mod br3_tests {
629 use super::*;
630
631 #[test]
635 fn a_single_label_names_the_positive_target() {
636 let p = assemble(" br3 r1, ahead\n halt\nahead: halt").unwrap();
637 assert_eq!(p.instructions[0].imm.value(), 2);
638 }
640
641 #[test]
643 fn a_backward_label_is_the_loop_idiom() {
644 let p = assemble("loop: addi r1, r1, -1\n br3 r1, loop\n halt").unwrap();
645 assert_eq!(p.instructions[1].imm.value(), -1);
646 }
647
648 #[test]
650 fn a_numeric_offset_is_accepted() {
651 let p = assemble("br3 r1, 4\nhalt").unwrap();
652 assert_eq!(p.instructions[0].imm.value(), 4);
653 }
654
655 #[test]
656 fn mirrored_targets_assemble() {
657 let src = "
659less: halt
660 halt
661 br3 r1, less, greater
662 halt
663greater: halt
664 ";
665 let p = assemble(src).unwrap();
666 assert_eq!(p.labels["less"], 0);
667 assert_eq!(p.labels["greater"], 4);
668 assert_eq!(p.instructions[2].imm.value(), 2);
669 }
670
671 #[test]
672 fn unmirrored_targets_are_rejected_with_both_positions() {
673 let src = "
676less: halt
677 br3 r1, less, greater
678 halt
679greater: halt
680 ";
681 let e = assemble(src).unwrap_err();
682 assert_eq!(
683 e.kind,
684 AsmErrorKind::Br3NotMirrored {
685 neg: 0,
686 reachable: -1,
687 pos: 3
688 }
689 );
690 assert!(e.to_string().contains("mirrored"), "{e}");
691 }
692
693 #[test]
694 fn brn_and_brp_take_ordinary_labels() {
695 let src = "
696 brn r1, less
697 brp r1, greater
698 halt
699less: halt
700greater: halt
701 ";
702 let p = assemble(src).unwrap();
703 assert_eq!(p.instructions[0].imm.value(), 3);
705 assert_eq!(p.instructions[1].imm.value(), 3);
706 }
707
708 #[test]
709 fn an_equate_resolves_to_its_value_not_an_offset() {
710 let p = assemble(
713 "
714 .equ BASE, -19683
715 addi r1, r0, BASE
716 addi r2, r0, here
717 here:
718 halt
719 ",
720 )
721 .unwrap();
722 assert_eq!(p.instructions[0].imm.value(), -19683);
723 assert_eq!(p.instructions[1].imm.value(), 1);
724 assert_eq!(p.equates["BASE"], -19683);
725 }
726
727 #[test]
728 fn directives_do_not_occupy_program_space() {
729 let p = assemble(
730 "
731 .equ A, 1
732 .equ B, 2
733 start:
734 addi r1, r0, A
735 halt
736 ",
737 )
738 .unwrap();
739 assert_eq!(p.instructions.len(), 2);
740 assert_eq!(p.labels["start"], 0);
741 }
742
743 #[test]
744 fn a_name_cannot_be_both_an_equate_and_a_label() {
745 let e = assemble(".equ dup, 1\ndup:\n halt").unwrap_err();
746 assert_eq!(e.kind, AsmErrorKind::DuplicateEquate("dup".into()));
747 }
748
749 #[test]
750 fn an_equate_defined_twice_is_rejected() {
751 let e = assemble(".equ A, 1\n.equ A, 2\nhalt").unwrap_err();
752 assert_eq!(e.kind, AsmErrorKind::DuplicateEquate("A".into()));
753 }
754
755 #[test]
756 fn a_malformed_equate_is_rejected() {
757 for src in [".equ A", ".equ A, B", ".equ 1, 2", ".equ A, 1, 2"] {
758 assert!(
759 matches!(
760 assemble(src).map(|_| ()).unwrap_err().kind,
761 AsmErrorKind::BadEquate(_)
762 ),
763 "{src}"
764 );
765 }
766 }
767
768 #[test]
769 fn a_mnemonic_beginning_with_the_directive_text_is_not_a_directive() {
770 let e = assemble(".equipment").unwrap_err();
772 assert!(matches!(e.kind, AsmErrorKind::UnknownMnemonic(_)), "{e:?}");
773 }
774
775 #[test]
776 fn an_undefined_name_is_still_an_error() {
777 let e = assemble("addi r1, r0, MISSING").unwrap_err();
778 assert_eq!(e.kind, AsmErrorKind::UndefinedLabel("MISSING".into()));
779 }
780
781 #[test]
782 fn a_name_may_carry_a_displacement() {
783 let p = assemble(
784 "
785 .equ WINDOW, 5
786 addi r1, r0, WINDOW
787 addi r2, r0, WINDOW+1
788 addi r3, r0, WINDOW-2
789 ",
790 )
791 .unwrap();
792 assert_eq!(p.instructions[0].imm.value(), 5);
793 assert_eq!(p.instructions[1].imm.value(), 6);
794 assert_eq!(p.instructions[2].imm.value(), 3);
795 }
796
797 #[test]
798 fn a_displacement_does_not_break_a_negative_literal() {
799 let p = assemble("addi r1, r0, -19683\nhalt").unwrap();
800 assert_eq!(p.instructions[0].imm.value(), -19683);
801 }
802
803 #[test]
804 fn a_label_may_carry_a_displacement() {
805 let p = assemble("here:\n addi r1, r0, 0\n jal r0, here+1\n halt").unwrap();
808 assert_eq!(p.instructions[1].imm.value(), 0, "here is -1 away, plus 1");
809 }
810
811 #[test]
812 fn a_malformed_displacement_is_rejected() {
813 assert!(matches!(
814 assemble(".equ A, 1\naddi r1, r0, A+x").unwrap_err().kind,
815 AsmErrorKind::BadImmediate(_)
816 ));
817 }
818}