Skip to main content

ternaria_isa/
asm.rs

1//! A small text assembler.
2//!
3//! Mnemonics and operand shapes are read from [`TABLE`] rather
4//! than from a list kept here, so the two cannot disagree. Adding an
5//! instruction to the `isa!` block makes it assemblable with no change here.
6//!
7//! # Syntax
8//!
9//! ```text
10//! ; comments start with ; or #
11//! start:                      ; a label
12//!     addi r1, r0, 10         ; registers are r-13 .. r13
13//! loop:
14//!     addi r1, r1, -1
15//!     br3  r1, loop           ; a label operand becomes a relative offset
16//!     halt
17//! ```
18//!
19//! A label used as an immediate resolves to the offset in instructions from
20//! the instruction referencing it. See [`ImmUnit`](crate::ImmUnit) for the
21//! units each instruction uses. Operands are separated by commas, whitespace,
22//! or both.
23//!
24//! # Named constants
25//!
26//! `.equ NAME, VALUE` binds a name to an absolute value. This is the difference
27//! between an equate and a label: a label resolves to an offset from the
28//! instruction that names it, an equate resolves to the value itself.
29//!
30//! ```text
31//! .equ CONSOLE_BASE, -19683
32//!     addi r1, r0, CONSOLE_BASE
33//! ```
34//!
35//! A name used as an immediate may carry a displacement: `BLOCK_WINDOW+1` is
36//! one more than the value of `BLOCK_WINDOW`. Only a single literal offset is
37//! accepted; this is not an expression language.
38//!
39//! Directives occupy no space in the program. A name may not be both an equate
40//! and a label. Device addresses are the main use: a crate that defines a
41//! device can emit the matching `.equ` lines so guest programs and the host
42//! agree by construction rather than by a literal copied into both.
43//!
44//! # Absolute label references
45//!
46//! `@label` resolves to the tryte address of a label rather than the distance
47//! to it. A trap vector needs this, since the handler's location must not
48//! depend on where the instruction naming it happens to sit.
49//!
50//! ```text
51//!     addi r1, r0, @handler
52//!     csrw r1, TVEC
53//! ```
54//!
55//! The address is measured from the base the program is assembled for, which
56//! is zero unless [`assemble_at`] is given another.
57//!
58//! # br3 and its two targets
59//!
60//! `br3` reaches two targets from one immediate, mirrored about the branch: it
61//! adds the immediate when the register is positive and subtracts it when the
62//! register is negative. Two forms are accepted.
63//!
64//! ```text
65//! br3 rs1, label              ; label is the POSITIVE target
66//! br3 rs1, neg_lbl, pos_lbl   ; both named; the assembler checks the mirror
67//! ```
68//!
69//! The one-label form suits a loop, where the label is behind the branch and
70//! only the positive case is reachable. The two-label form suits dispatch on
71//! [`Opcode::Cmp3`], and fails at assembly time if the
72//! targets are not mirrored rather than branching the wrong way at runtime.
73//!
74//! Where the targets cannot be mirrored, use `brn` and `brp`, which take one
75//! target each over the full immediate range.
76
77use crate::{Format, IMM_MAX, Imm, Instruction, Opcode, REG_MAX, Reg, TABLE};
78use std::collections::BTreeMap;
79use std::fmt;
80
81/// What went wrong, and where.
82#[derive(Clone, PartialEq, Eq, Debug)]
83pub struct AsmError {
84    /// Source line number, counting from 1.
85    pub line: usize,
86    /// What went wrong.
87    pub kind: AsmErrorKind,
88}
89
90/// The kinds of assembly failure.
91#[derive(Clone, PartialEq, Eq, Debug)]
92pub enum AsmErrorKind {
93    /// No instruction has this mnemonic.
94    UnknownMnemonic(String),
95    /// Wrong number of operands for the instruction's format.
96    WrongOperandCount {
97        /// The instruction.
98        mnemonic: &'static str,
99        /// How many it needs.
100        expected: usize,
101        /// How many it got.
102        got: usize,
103    },
104    /// An operand that should have been a register was not.
105    BadRegister(String),
106    /// An operand that should have been an immediate or label was not.
107    BadImmediate(String),
108    /// A label operand that never appears as a definition.
109    UndefinedLabel(String),
110    /// The same label defined twice.
111    DuplicateLabel(String),
112    /// The two `br3` targets are not mirrored about the branch.
113    Br3NotMirrored {
114        /// Instruction index of the negative target.
115        neg: i64,
116        /// Instruction index of the negative target that would be reached.
117        reachable: i64,
118        /// Instruction index of the positive target.
119        pos: i64,
120    },
121    /// The same name given to two `.equ` directives, or to an `.equ` and a
122    /// label.
123    DuplicateEquate(String),
124    /// An `.equ` directive whose operands are not a name and a number.
125    BadEquate(String),
126    /// A resolved offset or literal does not fit twelve trits.
127    ImmediateOutOfRange {
128        /// The value that did not fit.
129        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/// An assembled program.
176#[derive(Clone, PartialEq, Eq, Debug, Default)]
177pub struct Program {
178    /// The instructions, in order.
179    pub instructions: Vec<Instruction>,
180    /// Label name to instruction index.
181    pub labels: BTreeMap<String, usize>,
182    /// Name to value, from `.equ` directives.
183    pub equates: BTreeMap<String, i64>,
184}
185
186/// How many operands a format takes.
187const 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
198/// Strips comments and surrounding whitespace.
199fn clean(line: &str) -> &str {
200    let cut = line.find([';', '#']).unwrap_or(line.len());
201    line[..cut].trim()
202}
203
204/// Splits operands on commas and whitespace.
205fn 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
228/// Splits a trailing `+k` or `-k` off an operand.
229///
230/// Only one displacement is accepted and it must be a literal, so this is an
231/// offset from a name rather than an expression language. Anything more
232/// belongs in the program's own arithmetic, where it can be seen.
233fn split_displacement(tok: &str, line: usize) -> Result<(&str, i64), AsmError> {
234    // Skip position 0 so that a negative literal is not split into nothing.
235    let cut = tok
236        .rfind(['+', '-'])
237        // Position 0 is a sign on a literal, not a displacement.
238        .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
251/// The text after `.equ`, if the line is an `.equ` directive.
252fn equate_body(text: &str) -> Option<&str> {
253    let rest = text
254        .strip_prefix(".equ")
255        .or_else(|| text.strip_prefix(".EQU"))?;
256    // Reject `.equations`: the directive must be a whole word.
257    if rest.is_empty() || rest.starts_with([' ', '\t']) {
258        Some(rest.trim())
259    } else {
260        None
261    }
262}
263
264/// Parses the name and value of an `.equ` directive.
265fn 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
291/// Assembles source text.
292///
293/// Two passes. The first records where each label lands; the second emits
294/// instructions and resolves label references. One pass is insufficient because
295/// a forward branch names a label defined later.
296pub fn assemble(source: &str) -> Result<Program, AsmError> {
297    assemble_at(source, 0)
298}
299
300/// Assembles source that will be loaded at tryte address `base`.
301///
302/// `base` is what an absolute label reference resolves against. A program
303/// loaded at the reset vector uses zero, which is what [`assemble`] passes.
304pub fn assemble_at(source: &str, base: i64) -> Result<Program, AsmError> {
305    // Pass one: label positions, measured in instructions, and `.equ` values.
306    // Directives occupy no space, so they do not advance the index.
307    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        // A line may carry a label and an instruction: `loop: addi r1, r1, -1`
324        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    // A name that is both an equate and a label would resolve two ways.
340    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    // Pass two: emit.
348    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        // Resolves a token that may be a decimal literal or a label. A label
391        // becomes the offset in instructions from this instruction.
392        let imm_of = |tok: &str| -> Result<Imm, AsmError> {
393            // A name may carry a small displacement: `BLOCK_WINDOW+1`. Device
394            // registers sit one tryte apart, and writing the neighbour as a
395            // literal reintroduces exactly the drift named constants remove.
396            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                // The address of a label rather than the distance to it. A
401                // trap vector needs this: the handler's location does not
402                // depend on where the instruction naming it happens to sit.
403                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                // An equate is an absolute value, unlike a label.
410                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        // br3 reaches two targets from one immediate, mirrored about the
434        // branch. Naming both lets the assembler check the mirror holds
435        // instead of silently emitting a branch that goes the wrong way.
436        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
496/// Disassembles a run of instructions, one per line, with indices.
497pub 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        // br3 sits at index 2 and targets index 1, so the offset is -1.
543        assert_eq!(p.instructions[2].imm.value(), -1);
544        // jal sits at index 3 and targets index 0, so -3.
545        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        // Assemble -> disassemble -> assemble must reach the same instruction.
568        // Comparing the instructions, not just the count, is the point: an
569        // earlier version of this test only checked that something parsed,
570        // which would have passed even if the text said the wrong thing.
571        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        // The indexed listing is one line per instruction.
579        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    /// The assembler reads mnemonics from TABLE, so every instruction in the
618    /// set must be assemblable without the assembler knowing about it.
619    #[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    /// A single label names the positive target. The negative branch reaches
632    /// its mirror, which is why a forward label cannot serve as an error exit
633    /// for a negative value; that case wants brn.
634    #[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        // Positive reaches index 2. Negative reaches index -2, not index 2.
639    }
640
641    /// The loop idiom: the label is behind, so only the positive case is taken.
642    #[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    /// A numeric offset still works, since it states the mirror explicitly.
649    #[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        // less at 0, branch at 2, greater at 4: mirrored about the branch.
658        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        // less at 0, branch at 1, greater at 3: the negative branch would
674        // reach -1, not 0.
675        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        // brn at 0 targets index 3, brp at 1 targets index 4.
704        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        // The distinction from a label: `here` is two instructions ahead of
711        // the first addi, while BASE is -19683 wherever it is used.
712        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        // `.equ` must be a whole word, so `.equipment` is not swallowed.
771        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        // A branch that means "two past the label", which a dispatch table
806        // needs and which no arithmetic in the program can express.
807        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}