Skip to main content

ternaria_vm/
lib.rs

1//! The reference interpreter.
2//!
3//! A fetch, decode, execute loop written for clarity rather than speed. Later
4//! implementations, including the phase 7 JIT, are checked against it.
5//!
6//! # Machine state
7//!
8//! - 27 registers numbered -13 to 13 (D-05). `r0` reads as zero and discards
9//!   writes.
10//! - `pc` holds the tryte address of the instruction being executed. It
11//!   advances by 3 trytes unless an instruction redirects it.
12//! - Memory is passed in rather than owned, so one [`Cpu`] can run against
13//!   different memories and state stays separable from storage.
14//!
15//! # Traps
16//!
17//! Every abnormal condition raises a [`Trap`] carrying the `pc` that caused it.
18//! Arithmetic overflow traps rather than wrapping, uninitialised reads trap
19//! rather than returning zero, and undefined opcodes trap.
20//!
21//! Wrapping arithmetic exists in `ternaria-arith` and can be exposed as
22//! separate opcodes if a program needs it.
23
24#![forbid(unsafe_code)]
25#![warn(missing_docs)]
26
27use std::fmt;
28
29use ternaria_arith::{Trit, Tryte, Word};
30use ternaria_isa::REG_COUNT;
31use ternaria_mem::{Addressable, MemoryError, WORD_TRYTES};
32
33// Re-exported so a caller needs only this crate. `DecodeError` and
34// `MemoryError` appear in [`Trap`], so they are public regardless.
35pub mod csr;
36pub mod mmu;
37
38pub use csr::{Cause, Csrs, Priv};
39pub use mmu::{Access, Entry as PageEntry};
40pub use ternaria_isa::{DecodeError, Imm, Instruction, Opcode, Reg, asm};
41pub use ternaria_mem::MemoryError as MemError;
42
43/// Something stopped execution.
44#[derive(Clone, Copy, PartialEq, Eq, Debug)]
45pub enum Trap {
46    /// The word at `pc` is not a valid instruction.
47    IllegalInstruction {
48        /// Where it happened.
49        pc: i64,
50        /// Why the decode failed.
51        cause: DecodeError,
52    },
53    /// A memory access failed.
54    Memory {
55        /// Where it happened.
56        pc: i64,
57        /// Why the access failed.
58        cause: MemoryError,
59    },
60    /// Division or remainder by zero.
61    DivideByZero {
62        /// Where it happened.
63        pc: i64,
64    },
65    /// A result left the 27-trit range.
66    Overflow {
67        /// Where it happened.
68        pc: i64,
69        /// Which operation overflowed.
70        op: Opcode,
71    },
72    /// Control flow left the address space.
73    PcOutOfRange {
74        /// Where it happened.
75        pc: i64,
76    },
77    /// An `ecall`. Not a fault: the deliberate way into the operating system.
78    Ecall {
79        /// Where it happened.
80        pc: i64,
81    },
82    /// An instruction needed a privilege the machine was not at.
83    Privilege {
84        /// Where it happened.
85        pc: i64,
86        /// The level the instruction required.
87        needed: Priv,
88    },
89    /// Address translation failed.
90    PageFault {
91        /// Where it happened.
92        pc: i64,
93        /// The virtual address that could not be translated.
94        addr: i64,
95    },
96    /// [`Cpu::run`] reached its step budget without halting.
97    StepLimit {
98        /// Where it was when the budget ran out.
99        pc: i64,
100        /// How many steps had run.
101        steps: u64,
102    },
103}
104
105impl fmt::Display for Trap {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        match self {
108            Trap::IllegalInstruction { pc, cause } => {
109                write!(f, "illegal instruction at pc={pc}: {cause}")
110            }
111            Trap::Memory { pc, cause } => write!(f, "memory fault at pc={pc}: {cause}"),
112            Trap::DivideByZero { pc } => write!(f, "divide by zero at pc={pc}"),
113            Trap::Overflow { pc, op } => {
114                write!(f, "arithmetic overflow in {} at pc={pc}", op.mnemonic())
115            }
116            Trap::PcOutOfRange { pc } => write!(f, "pc left the address space at pc={pc}"),
117            Trap::Ecall { pc } => write!(f, "ecall at pc={pc} with no trap vector set"),
118            Trap::Privilege { pc, needed } => {
119                write!(f, "instruction at pc={pc} requires {needed:?} privilege")
120            }
121            Trap::PageFault { pc, addr } => {
122                write!(f, "page fault on address {addr} at pc={pc}")
123            }
124            Trap::StepLimit { pc, steps } => {
125                write!(f, "step limit reached after {steps} steps at pc={pc}")
126            }
127        }
128    }
129}
130
131impl std::error::Error for Trap {}
132
133/// The cause, the value the handler sees in `tval`, and the pc to save.
134fn describe(trap: Trap) -> (Cause, i64, i64) {
135    match trap {
136        Trap::IllegalInstruction { pc, .. } => (Cause::IllegalInstruction, 0, pc),
137        Trap::Memory { pc, cause } => (Cause::Memory, faulting_address(cause), pc),
138        Trap::DivideByZero { pc } => (Cause::DivideByZero, 0, pc),
139        Trap::Overflow { pc, op } => (Cause::Overflow, op.value(), pc),
140        Trap::PcOutOfRange { pc } => (Cause::PcOutOfRange, pc, pc),
141        Trap::Ecall { pc } => (Cause::Ecall, 0, pc),
142        Trap::Privilege { pc, needed } => {
143            (Cause::PrivilegeViolation, needed.trit() as i8 as i64, pc)
144        }
145        Trap::PageFault { pc, addr } => (Cause::PageFault, addr, pc),
146        // Filtered out before this is reached.
147        Trap::StepLimit { pc, .. } => (Cause::IllegalInstruction, 0, pc),
148    }
149}
150
151/// The address a memory error names, for `tval`.
152fn faulting_address(e: MemoryError) -> i64 {
153    match e {
154        MemoryError::Uninitialised { addr }
155        | MemoryError::Misaligned { addr }
156        | MemoryError::Device { addr, .. }
157        | MemoryError::AddressOverflow { addr, .. } => addr,
158        MemoryError::NotAByte { value } => value,
159    }
160}
161
162/// Result of a completed run.
163#[derive(Clone, Copy, PartialEq, Eq, Debug)]
164pub struct Halted {
165    /// How many instructions executed.
166    pub steps: u64,
167}
168
169/// The processor.
170#[derive(Clone)]
171pub struct Cpu {
172    regs: [Word; REG_COUNT],
173    pc: Word,
174    halted: bool,
175    steps: u64,
176    /// Control and status registers. Public so a host can set a trap vector or
177    /// inspect a cause without going through guest instructions.
178    pub csrs: Csrs,
179    privilege: Priv,
180}
181
182impl Cpu {
183    /// A processor with every register zero, starting at `entry`.
184    pub fn new(entry: Word) -> Cpu {
185        Cpu {
186            regs: [Word::ZERO; REG_COUNT],
187            pc: entry,
188            halted: false,
189            steps: 0,
190            csrs: Csrs::default(),
191            // Reset is at machine level, so a program loaded without an
192            // operating system under it can still reach devices.
193            privilege: Priv::Machine,
194        }
195    }
196
197    /// Reads a register. `r0` always reads zero.
198    #[inline]
199    pub fn reg(&self, r: Reg) -> Word {
200        if r == Reg::ZERO {
201            Word::ZERO
202        } else {
203            self.regs[r.index()]
204        }
205    }
206
207    /// Writes a register. Writes to `r0` are discarded, which lets `r0` serve
208    /// as a destination for instructions run only for their side effects, such
209    /// as `jalr r0, r3, 0` to return without saving a link.
210    #[inline]
211    pub fn set_reg(&mut self, r: Reg, value: Word) {
212        if r != Reg::ZERO {
213            self.regs[r.index()] = value;
214        }
215    }
216
217    /// The program counter - the tryte address of the next instruction.
218    pub fn pc(&self) -> Word {
219        self.pc
220    }
221
222    /// Sets the program counter.
223    pub fn set_pc(&mut self, pc: Word) {
224        self.pc = pc;
225    }
226
227    /// True once a `halt` has executed.
228    pub fn halted(&self) -> bool {
229        self.halted
230    }
231
232    /// How many instructions have executed.
233    pub fn steps(&self) -> u64 {
234        self.steps
235    }
236
237    /// The current privilege level.
238    pub fn privilege(&self) -> Priv {
239        self.privilege
240    }
241
242    /// Sets the privilege level directly.
243    ///
244    /// For a host bringing a machine up, or a test. A guest changes privilege
245    /// only by taking a trap or executing `tret`.
246    pub fn set_privilege(&mut self, level: Priv) {
247        self.privilege = level;
248    }
249
250    /// The value in register `n`, numbered -13 to 13.
251    ///
252    /// # Panics
253    /// If `n` is outside the register range.
254    pub fn reg_value(&self, n: i8) -> i64 {
255        self.reg(Reg::new(n).expect("register number out of range"))
256            .value()
257    }
258
259    /// Loads a word through translation, as an instruction would.
260    ///
261    /// Exposed so a host or a test can see what an address means at the
262    /// machine's current privilege without assembling a program to do it.
263    pub fn load_word<A: Addressable>(&self, virt: i64, mem: &mut A) -> Result<Word, Trap> {
264        let addr = self.access(virt, Access::Read, mem)?;
265        mem.read_word(addr).map_err(|cause| Trap::Memory {
266            pc: self.pc.value(),
267            cause,
268        })
269    }
270
271    /// Turns a virtual address into a physical one.
272    ///
273    /// Translation is off at machine level and off whenever `ptbr` is zero,
274    /// which is the reset state. Both cases are the identity, so a program
275    /// loaded without an operating system under it never walks a table.
276    fn translate<A: Addressable>(
277        &self,
278        virt: i64,
279        access: Access,
280        mem: &mut A,
281    ) -> Result<i64, Trap> {
282        if self.privilege == Priv::Machine || self.csrs.ptbr == 0 {
283            return Ok(virt);
284        }
285        let pc = self.pc.value();
286        let fault = || Trap::PageFault { pc, addr: virt };
287        let va = Word::new(virt).ok_or_else(fault)?;
288        if !mmu::is_canonical(va) {
289            return Err(fault());
290        }
291
292        let mut table = self.csrs.ptbr;
293        for level in (0..mmu::LEVELS).rev() {
294            let slot = table
295                .checked_add(
296                    mmu::index(va, level)
297                        .checked_mul(WORD_TRYTES)
298                        .ok_or_else(fault)?,
299                )
300                .ok_or_else(fault)?;
301            let word = mem
302                .read_word(Word::new(slot).ok_or_else(fault)?)
303                .map_err(|_| fault())?;
304            let entry = mmu::Entry(word);
305            if !entry.flag(mmu::flag::VALID) {
306                return Err(fault());
307            }
308            if entry.flag(mmu::flag::LEAF) {
309                if !entry.flag(access.flag()) {
310                    return Err(fault());
311                }
312                if self.privilege == Priv::User && !entry.flag(mmu::flag::USER) {
313                    return Err(fault());
314                }
315                // A leaf above level 0 maps a superpage, so the index trits the
316                // walk never reached stay part of the offset.
317                let base = entry
318                    .ppn()
319                    .checked_mul(mmu::PAGE_TRYTES)
320                    .ok_or_else(fault)?;
321                return base
322                    .checked_add(mmu::passthrough(va, level))
323                    .ok_or_else(fault);
324            }
325            table = entry
326                .ppn()
327                .checked_mul(mmu::PAGE_TRYTES)
328                .ok_or_else(fault)?;
329        }
330        // Every level was a table and none was a leaf.
331        Err(fault())
332    }
333
334    /// Translates and checks that the access is one this privilege may make.
335    fn access<A: Addressable>(&self, virt: i64, access: Access, mem: &mut A) -> Result<Word, Trap> {
336        let phys = self.translate(virt, access, mem)?;
337        let addr = Word::new(phys).ok_or(Trap::Memory {
338            pc: self.pc.value(),
339            cause: MemoryError::AddressOverflow {
340                addr: phys,
341                len: WORD_TRYTES,
342            },
343        })?;
344        if mem.is_privileged(addr) {
345            self.require(Priv::Supervisor, self.pc.value())?;
346        }
347        Ok(addr)
348    }
349
350    /// Fails unless the machine is at `needed` or above.
351    fn require(&self, needed: Priv, pc: i64) -> Result<(), Trap> {
352        if self.privilege >= needed {
353            Ok(())
354        } else {
355            Err(Trap::Privilege { pc, needed })
356        }
357    }
358
359    /// Moves `pc` by a number of instructions, scaling by 3 to get trytes.
360    /// Branch targets are therefore word-aligned by construction.
361    fn jump_by(&mut self, instructions: i64) -> Result<(), Trap> {
362        let here = self.pc.value();
363        let target = instructions
364            .checked_mul(WORD_TRYTES)
365            .and_then(|d| here.checked_add(d))
366            .ok_or(Trap::PcOutOfRange { pc: here })?;
367        self.pc = Word::new(target).ok_or(Trap::PcOutOfRange { pc: here })?;
368        Ok(())
369    }
370
371    /// Executes one instruction. Returns `Ok(false)` once halted; further
372    /// calls do nothing.
373    ///
374    /// # Traps
375    ///
376    /// What a trap does depends on whether an operating system has claimed
377    /// them. With `tvec` zero, which is the reset state, a trap stops the
378    /// machine and is returned to the host. With `tvec` set, the trap is
379    /// vectored to the handler and `step` returns normally, so a program that
380    /// faults no longer takes the machine down.
381    pub fn step<A: Addressable>(&mut self, mem: &mut A) -> Result<bool, Trap> {
382        if self.halted {
383            return Ok(false);
384        }
385
386        // An interrupt is taken between instructions, before the fetch, so the
387        // saved pc names the instruction that has not run rather than one that
388        // has half run.
389        self.csrs.ip = mem.interrupts();
390        if let Some(cause) = self.csrs.pending_interrupt()
391            && self.csrs.tvec != 0
392        {
393            self.enter_trap(cause, 0, self.pc.value())?;
394            return Ok(true);
395        }
396
397        match self.step_once(mem) {
398            Ok(()) => {
399                self.steps += 1;
400                Ok(!self.halted)
401            }
402            Err(trap) => {
403                self.steps += 1;
404                self.take(trap).map(|()| !self.halted)
405            }
406        }
407    }
408
409    /// One fetch, decode and execute, with no trap handling.
410    fn step_once<A: Addressable>(&mut self, mem: &mut A) -> Result<(), Trap> {
411        let here = self.pc.value();
412        let phys = self.translate(here, Access::Execute, mem)?;
413        let word = mem
414            .read_word(Word::from_value(phys))
415            .map_err(|cause| Trap::Memory { pc: here, cause })?;
416        let instr = Instruction::decode(word)
417            .map_err(|cause| Trap::IllegalInstruction { pc: here, cause })?;
418        self.execute(instr, mem)
419    }
420
421    /// Vectors a trap to the handler, or passes it to the host if none is set.
422    fn take(&mut self, trap: Trap) -> Result<(), Trap> {
423        // The step limit is a property of the host's budget, not of the guest,
424        // so there is nothing for a guest handler to do about it.
425        if self.csrs.tvec == 0 || matches!(trap, Trap::StepLimit { .. }) {
426            return Err(trap);
427        }
428        let (cause, tval, pc) = describe(trap);
429        self.enter_trap(cause, tval, pc)
430    }
431
432    /// Saves the interrupted state and enters the handler at machine level.
433    fn enter_trap(&mut self, cause: Cause, tval: i64, pc: i64) -> Result<(), Trap> {
434        self.csrs.tepc = pc;
435        self.csrs.tcause = cause.value();
436        self.csrs.tval = tval;
437        self.csrs.tpriv = self.privilege;
438        // Interrupts off inside the handler, restored by `tret`. A pending
439        // source is level-triggered, so without this it would re-fire before
440        // the handler's first instruction and the handler would never run.
441        self.csrs.tie = self.csrs.ie;
442        self.csrs.ie = Word::ZERO;
443        self.privilege = Priv::Machine;
444        self.pc = Word::new(self.csrs.tvec).ok_or(Trap::PcOutOfRange { pc: self.csrs.tvec })?;
445        Ok(())
446    }
447
448    /// Runs until halt or trap, at most `max_steps` instructions. The budget
449    /// bounds programs that do not terminate.
450    pub fn run<A: Addressable>(&mut self, mem: &mut A, max_steps: u64) -> Result<Halted, Trap> {
451        while self.steps < max_steps {
452            if !self.step(mem)? {
453                return Ok(Halted { steps: self.steps });
454            }
455        }
456        Err(Trap::StepLimit {
457            pc: self.pc.value(),
458            steps: self.steps,
459        })
460    }
461
462    /// Applies one decoded instruction.
463    fn execute<A: Addressable>(&mut self, instr: Instruction, mem: &mut A) -> Result<(), Trap> {
464        let here = self.pc.value();
465        let op = instr.op;
466        let a = self.reg(instr.rs1);
467        let b = self.reg(instr.rs2);
468        let imm = instr.imm.value() as i64;
469
470        // Overflow is a trap, never a wrap.
471        let checked =
472            |r: Option<Word>| -> Result<Word, Trap> { r.ok_or(Trap::Overflow { pc: here, op }) };
473
474        // Control-flow arms set pc and return early; the rest fall through.
475        match op {
476            Opcode::Nop => {}
477            Opcode::Add => {
478                let v = checked(a.checked_add(b))?;
479                self.set_reg(instr.rd, v);
480            }
481            Opcode::Sub => {
482                let v = checked(a.checked_sub(b))?;
483                self.set_reg(instr.rd, v);
484            }
485            Opcode::Mul => {
486                let v = checked(a.checked_mul(b))?;
487                self.set_reg(instr.rd, v);
488            }
489            Opcode::Div => {
490                let v = a.checked_div(b).ok_or(Trap::DivideByZero { pc: here })?;
491                self.set_reg(instr.rd, v);
492            }
493            Opcode::Rem => {
494                let v = a.checked_rem(b).ok_or(Trap::DivideByZero { pc: here })?;
495                self.set_reg(instr.rd, v);
496            }
497            Opcode::Min => self.set_reg(instr.rd, a.trit_and(b)),
498            Opcode::Max => self.set_reg(instr.rd, a.trit_or(b)),
499            Opcode::Cmp3 => {
500                let t = a.cmp3(b);
501                self.set_reg(instr.rd, Word::from_value(t.value() as i64));
502            }
503            // Neither can overflow; the range is symmetric.
504            Opcode::Neg => self.set_reg(instr.rd, a.neg()),
505            Opcode::Abs => self.set_reg(instr.rd, a.abs()),
506            Opcode::Addi => {
507                let v = checked(a.checked_add(Word::from_value(imm)))?;
508                self.set_reg(instr.rd, v);
509            }
510            // A negative shift distance shifts the other way.
511            Opcode::Shl => {
512                let v = if imm >= 0 {
513                    checked(a.checked_shl_trits(imm as u32))?
514                } else {
515                    a.shr_trits((-imm) as u32)
516                };
517                self.set_reg(instr.rd, v);
518            }
519            Opcode::Shr => {
520                let v = if imm >= 0 {
521                    a.shr_trits(imm as u32)
522                } else {
523                    checked(a.checked_shl_trits((-imm) as u32))?
524                };
525                self.set_reg(instr.rd, v);
526            }
527            Opcode::Lw => {
528                let virt = checked(a.checked_add(Word::from_value(imm)))?;
529                let addr = self.access(virt.value(), Access::Read, mem)?;
530                let v = mem
531                    .read_word(addr)
532                    .map_err(|cause| Trap::Memory { pc: here, cause })?;
533                self.set_reg(instr.rd, v);
534            }
535            Opcode::Lt => {
536                let virt = checked(a.checked_add(Word::from_value(imm)))?;
537                let addr = self.access(virt.value(), Access::Read, mem)?;
538                let v = mem
539                    .read_tryte(addr)
540                    .map_err(|cause| Trap::Memory { pc: here, cause })?;
541                self.set_reg(instr.rd, Word::from_value(v.value() as i64));
542            }
543            Opcode::St => {
544                let virt = checked(a.checked_add(Word::from_value(imm)))?;
545                let addr = self.access(virt.value(), Access::Write, mem)?;
546                let v = self.reg(instr.rd);
547                // Values outside the tryte range are a program error rather
548                // than a silent truncation.
549                let t = i32::try_from(v.value())
550                    .ok()
551                    .and_then(Tryte::new)
552                    .ok_or(Trap::Overflow { pc: here, op })?;
553                mem.write_tryte(addr, t)
554                    .map_err(|cause| Trap::Memory { pc: here, cause })?;
555            }
556            Opcode::Sw => {
557                let virt = checked(a.checked_add(Word::from_value(imm)))?;
558                let addr = self.access(virt.value(), Access::Write, mem)?;
559                let v = self.reg(instr.rd);
560                mem.write_word(addr, v)
561                    .map_err(|cause| Trap::Memory { pc: here, cause })?;
562            }
563            Opcode::Jal => {
564                let link = self.next_pc()?;
565                self.set_reg(instr.rd, link);
566                self.jump_by(imm)?;
567                return Ok(());
568            }
569            Opcode::Jalr => {
570                let link = self.next_pc()?;
571                // Read the target before writing rd, or `jalr r1, r1, 0` would
572                // read back its own link.
573                let target = a.value();
574                self.set_reg(instr.rd, link);
575                let dest = imm
576                    .checked_mul(WORD_TRYTES)
577                    .and_then(|d| target.checked_add(d))
578                    .ok_or(Trap::PcOutOfRange { pc: here })?;
579                self.pc = Word::new(dest).ok_or(Trap::PcOutOfRange { pc: here })?;
580                return Ok(());
581            }
582            Opcode::Br3 => {
583                // Forward if positive, backward if negative, next if zero.
584                match a.signum() {
585                    Trit::Pos => self.jump_by(imm)?,
586                    Trit::Neg => self.jump_by(-imm)?,
587                    Trit::Zero => self.jump_by(1)?,
588                }
589                return Ok(());
590            }
591            Opcode::Brz => {
592                let taken = a.signum() == Trit::Zero;
593                self.jump_by(if taken { imm } else { 1 })?;
594                return Ok(());
595            }
596            // brn and brp take a single target each, for the cases where the
597            // two br3 targets cannot be mirrored about the branch.
598            Opcode::Brn => {
599                let taken = a.signum() == Trit::Neg;
600                self.jump_by(if taken { imm } else { 1 })?;
601                return Ok(());
602            }
603            Opcode::Csrr => {
604                self.require(Priv::Supervisor, here)?;
605                let v = self
606                    .csrs
607                    .read(imm, self.privilege)
608                    .ok_or(Trap::IllegalInstruction {
609                        pc: here,
610                        cause: DecodeError::UnknownOpcode { value: imm },
611                    })?;
612                self.set_reg(instr.rd, Word::from_value(v));
613            }
614            Opcode::Csrw => {
615                self.require(Priv::Supervisor, here)?;
616                self.csrs
617                    .write(imm, a.value())
618                    .ok_or(Trap::IllegalInstruction {
619                        pc: here,
620                        cause: DecodeError::UnknownOpcode { value: imm },
621                    })?;
622            }
623            Opcode::Tret => {
624                self.require(Priv::Supervisor, here)?;
625                self.privilege = self.csrs.tpriv;
626                self.csrs.ie = self.csrs.tie;
627                self.pc =
628                    Word::new(self.csrs.tepc).ok_or(Trap::PcOutOfRange { pc: self.csrs.tepc })?;
629                return Ok(());
630            }
631            // Not a fault. The vectoring in `step` treats it like one because
632            // the mechanism is identical; only the cause differs.
633            Opcode::Ecall => return Err(Trap::Ecall { pc: here }),
634            Opcode::Brp => {
635                let taken = a.signum() == Trit::Pos;
636                self.jump_by(if taken { imm } else { 1 })?;
637                return Ok(());
638            }
639            Opcode::Halt => {
640                self.halted = true;
641                self.steps += 1;
642                return Ok(());
643            }
644        }
645
646        self.jump_by(1)
647    }
648
649    /// The address of the following instruction, for a link register.
650    fn next_pc(&self) -> Result<Word, Trap> {
651        let here = self.pc.value();
652        here.checked_add(WORD_TRYTES)
653            .and_then(Word::new)
654            .ok_or(Trap::PcOutOfRange { pc: here })
655    }
656}
657
658/// Writes instructions into memory as consecutive words. `at` must be
659/// word-aligned.
660pub fn load<A: Addressable>(
661    mem: &mut A,
662    at: Word,
663    program: &[Instruction],
664) -> Result<(), MemoryError> {
665    let mut addr = at.value();
666    for instr in program {
667        mem.write_word(Word::from_value(addr), instr.encode())?;
668        addr += WORD_TRYTES;
669    }
670    Ok(())
671}
672
673/// Assembles source, loads it at `at`, and returns a [`Cpu`] ready to run.
674pub fn assemble_and_load<A: Addressable>(
675    mem: &mut A,
676    at: Word,
677    source: &str,
678) -> Result<Cpu, Box<dyn std::error::Error>> {
679    // Assembled for where it is loaded, so `@label` names a real address.
680    let program = ternaria_isa::asm::assemble_at(source, at.value())?;
681    load(mem, at, &program.instructions)?;
682    Ok(Cpu::new(at))
683}