Skip to main content

ternaria_mem/
lib.rs

1//! Guest memory for the Ternaria emulator (D-14).
2//!
3//! # Shape
4//!
5//! - The addressable unit is the 9-trit [`Tryte`] (D-02).
6//! - Addresses are signed, covering +/-3.81*10^12 with zero at the centre.
7//! - Little-endian: the least significant tryte is at the lowest address. This
8//!   matches `ternaria-arith`, which orders trits least-significant-first.
9//! - Everything aligns to the word (3 trytes), including double words. See
10//!   [`Memory::read_double`].
11//! - Sparse. The full address space is about 8.6 TB at BCT density, so pages
12//!   are allocated on first write and never materialised for reads.
13//! - Memory that has never been written reads as an error rather than zero.
14//!
15//! # Storage format
16//!
17//! Trytes are stored as BCT rather than as numeric values. Every `i32` is a
18//! valid tryte value, so a value representation would leave nowhere to record
19//! that a location has never been written. BCT has a spare codepoint for it
20//! (D-03).
21
22#![forbid(unsafe_code)]
23#![warn(missing_docs)]
24
25pub mod bytes;
26
27use std::collections::BTreeMap;
28use std::fmt;
29
30use ternaria_arith::{DoubleWord, Tryte, Word};
31
32/// Trytes per page: 3^6 = 729 (D-14). The smaller of the two candidates
33/// considered; smaller pages waste less on a sparse address space at the cost
34/// of more host allocations.
35pub const PAGE_TRYTES: usize = 729;
36
37/// Trytes per word - 27 trits / 9.
38pub const WORD_TRYTES: i64 = 3;
39
40/// Trytes per double word - 54 trits / 9.
41pub const DOUBLE_TRYTES: i64 = 6;
42
43/// A tryte of pure NaT: all nine trits carry the `10` poison pattern.
44///
45/// `0b10` repeated nine times across 18 bits.
46const NAT_TRYTE: u32 = 0b10_1010_1010_1010_1010;
47
48/// Something went wrong with a memory access.
49#[derive(Clone, Copy, PartialEq, Eq, Debug)]
50pub enum MemoryError {
51    /// Read of memory that was never written.
52    Uninitialised {
53        /// The offending address.
54        addr: i64,
55    },
56    /// Access not aligned to a word boundary.
57    Misaligned {
58        /// The offending address.
59        addr: i64,
60    },
61    /// A tryte outside 0..=255 was written to a byte-oriented device (D-04).
62    NotAByte {
63        /// The offending value.
64        value: i64,
65    },
66    /// A device rejected the access.
67    Device {
68        /// The address accessed.
69        addr: i64,
70        /// What the device reported.
71        reason: &'static str,
72    },
73    /// The access would run off the end of the address space.
74    AddressOverflow {
75        /// The base address of the access.
76        addr: i64,
77        /// How many trytes the access needed.
78        len: i64,
79    },
80}
81
82impl MemoryError {
83    /// For an [`AddressOverflow`](MemoryError::AddressOverflow), how many
84    /// trytes past the end of the address space the access would have reached.
85    /// `None` for the other variants.
86    pub fn overshoot(&self) -> Option<i64> {
87        match *self {
88            MemoryError::AddressOverflow { addr, len } => {
89                let last = addr.saturating_add(len - 1);
90                Some((last - Word::MAX.value()).max(0))
91            }
92            _ => None,
93        }
94    }
95}
96
97impl fmt::Display for MemoryError {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        match self {
100            MemoryError::Uninitialised { addr } => {
101                write!(f, "read of uninitialised memory at {addr}")
102            }
103            MemoryError::Misaligned { addr } => {
104                write!(f, "unaligned access at {addr}; must be a multiple of 3")
105            }
106            MemoryError::NotAByte { value } => {
107                write!(f, "value {value} is outside 0..=255 and is not a byte")
108            }
109            MemoryError::Device { addr, reason } => {
110                write!(f, "device at {addr} rejected the access: {reason}")
111            }
112            MemoryError::AddressOverflow { addr, len } => {
113                write!(
114                    f,
115                    "access of {len} trytes at {addr} leaves the address space by {} tryte(s)",
116                    self.overshoot().unwrap_or(0)
117                )
118            }
119        }
120    }
121}
122
123impl std::error::Error for MemoryError {}
124
125/// One page of guest memory. Boxed by [`Memory`] to keep the map nodes small.
126struct Page {
127    trytes: [u32; PAGE_TRYTES],
128}
129
130impl Page {
131    /// A fresh page is entirely poison.
132    fn poisoned() -> Box<Page> {
133        Box::new(Page {
134            trytes: [NAT_TRYTE; PAGE_TRYTES],
135        })
136    }
137}
138
139/// Sparse, BCT-backed guest memory.
140#[derive(Default)]
141pub struct Memory {
142    pages: BTreeMap<i64, Box<Page>>,
143}
144
145impl Memory {
146    /// Empty memory. Every address reads as uninitialised.
147    pub fn new() -> Self {
148        Memory {
149            pages: BTreeMap::new(),
150        }
151    }
152
153    /// How many pages are currently allocated. For tests and diagnostics.
154    pub fn pages_allocated(&self) -> usize {
155        self.pages.len()
156    }
157
158    /// Splits an address into (page index, offset within page).
159    ///
160    /// Euclidean rather than truncating division, so the offset is in
161    /// `0..PAGE_TRYTES` for negative addresses as well. Truncating division
162    /// would produce negative offsets.
163    #[inline]
164    fn split(addr: i64) -> (i64, usize) {
165        let page = addr.div_euclid(PAGE_TRYTES as i64);
166        let offset = addr.rem_euclid(PAGE_TRYTES as i64) as usize;
167        (page, offset)
168    }
169
170    /// True when `addr` is word-aligned. Equivalent to testing whether the low
171    /// trit is zero. Applies at every width, since everything aligns to the
172    /// word (D-14).
173    #[inline]
174    pub fn is_aligned(addr: Word) -> bool {
175        addr.value().rem_euclid(WORD_TRYTES) == 0
176    }
177
178    /// Checks that `len` trytes starting at `addr` stay inside the space.
179    fn check_span(addr: Word, len: i64) -> Result<i64, MemoryError> {
180        let base = addr.value();
181        let last = base
182            .checked_add(len - 1)
183            .ok_or(MemoryError::AddressOverflow { addr: base, len })?;
184        if Word::new(last).is_none() {
185            return Err(MemoryError::AddressOverflow { addr: base, len });
186        }
187        Ok(base)
188    }
189
190    /// Reads one tryte. Errors if it was never written.
191    pub fn read_tryte(&self, addr: Word) -> Result<Tryte, MemoryError> {
192        let a = Self::check_span(addr, 1)?;
193        let (page, offset) = Self::split(a);
194        // An unallocated page is unwritten, so there is no need to
195        // materialise a poisoned page in order to fail on it.
196        let bits = match self.pages.get(&page) {
197            Some(p) => p.trytes[offset],
198            None => return Err(MemoryError::Uninitialised { addr: a }),
199        };
200        // Mapping NatError to Uninitialised is exact, given one invariant:
201        // page poisoning is the only way a NaT code enters memory.
202        // `write_tryte` stores `Tryte::to_bct()`, and a `Tryte` is nine valid
203        // trits, so no write can produce the 10 pattern.
204        // `no_write_can_produce_nat` checks this.
205        //
206        // A future path that writes raw BCT, such as a loader or a snapshot
207        // restore, must reject NaT itself or this mapping becomes wrong.
208        Tryte::from_bct(bits).map_err(|_| MemoryError::Uninitialised { addr: a })
209    }
210
211    /// Writes one tryte, allocating its page if needed.
212    pub fn write_tryte(&mut self, addr: Word, value: Tryte) -> Result<(), MemoryError> {
213        let a = Self::check_span(addr, 1)?;
214        let (page, offset) = Self::split(a);
215        let p = self.pages.entry(page).or_insert_with(Page::poisoned);
216        p.trytes[offset] = value.to_bct();
217        Ok(())
218    }
219
220    /// True when this address holds a value that was actually written.
221    pub fn is_initialised(&self, addr: Word) -> bool {
222        self.read_tryte(addr).is_ok()
223    }
224
225    /// Reads a word: three trytes, least significant first.
226    ///
227    /// Requires word alignment. The tryte at `addr` holds trits 0..9, the next
228    /// holds 9..18, and the last holds 18..27, so the weights are 3^0, 3^9 and
229    /// 3^18.
230    pub fn read_word(&self, addr: Word) -> Result<Word, MemoryError> {
231        if !Self::is_aligned(addr) {
232            return Err(MemoryError::Misaligned { addr: addr.value() });
233        }
234        let base = Self::check_span(addr, WORD_TRYTES)?;
235        let mut trytes = [Tryte::ZERO; Word::TRYTES];
236        for (i, slot) in trytes.iter_mut().enumerate() {
237            *slot = self.read_tryte(Word::from_value(base + i as i64))?;
238        }
239        Ok(Word::from_trytes(trytes))
240    }
241
242    /// Writes a word: three trytes, least significant first.
243    pub fn write_word(&mut self, addr: Word, value: Word) -> Result<(), MemoryError> {
244        if !Self::is_aligned(addr) {
245            return Err(MemoryError::Misaligned { addr: addr.value() });
246        }
247        let base = Self::check_span(addr, WORD_TRYTES)?;
248        for i in 0..Word::TRYTES {
249            self.write_tryte(Word::from_value(base + i as i64), value.tryte(i))?;
250        }
251        Ok(())
252    }
253
254    /// Reads a double word: six trytes, least significant first.
255    ///
256    /// Aligned to the word rather than to its own six trytes. Six-tryte
257    /// alignment would require testing `addr mod 6 == 0`, which in base 3 means
258    /// checking that the low trit is zero and the digit sum is even, a 27-trit
259    /// reduction rather than a single trit test (D-14).
260    pub fn read_double(&self, addr: Word) -> Result<DoubleWord, MemoryError> {
261        if !Self::is_aligned(addr) {
262            return Err(MemoryError::Misaligned { addr: addr.value() });
263        }
264        let base = Self::check_span(addr, DOUBLE_TRYTES)?;
265        let mut trytes = [Tryte::ZERO; DoubleWord::TRYTES];
266        for (i, slot) in trytes.iter_mut().enumerate() {
267            *slot = self.read_tryte(Word::from_value(base + i as i64))?;
268        }
269        Ok(DoubleWord::from_trytes(trytes))
270    }
271
272    /// Writes a double word: six trytes, least significant first.
273    pub fn write_double(&mut self, addr: Word, value: DoubleWord) -> Result<(), MemoryError> {
274        if !Self::is_aligned(addr) {
275            return Err(MemoryError::Misaligned { addr: addr.value() });
276        }
277        let base = Self::check_span(addr, DOUBLE_TRYTES)?;
278        for i in 0..DoubleWord::TRYTES {
279            self.write_tryte(Word::from_value(base + i as i64), value.tryte(i))?;
280        }
281        Ok(())
282    }
283}
284
285/// Anything a program can load from and store to.
286///
287/// Reads take `&mut self` because a device read can have a side effect, such as
288/// consuming a byte from a console input queue.
289pub trait Addressable {
290    /// Reads one tryte.
291    fn read_tryte(&mut self, addr: Word) -> Result<Tryte, MemoryError>;
292    /// Writes one tryte.
293    fn write_tryte(&mut self, addr: Word, value: Tryte) -> Result<(), MemoryError>;
294    /// Reads a word: three trytes, least significant first.
295    fn read_word(&mut self, addr: Word) -> Result<Word, MemoryError>;
296    /// Writes a word: three trytes, least significant first.
297    fn write_word(&mut self, addr: Word, value: Word) -> Result<(), MemoryError>;
298
299    /// True if reaching `addr` should require supervisor privilege.
300    ///
301    /// Plain memory returns false for everything. A bus that maps devices
302    /// returns true for their addresses, which is what stops a user program
303    /// driving hardware directly.
304    fn is_privileged(&self, _addr: Word) -> bool {
305        false
306    }
307
308    /// Which interrupt sources are asserting, one trit per source.
309    ///
310    /// See [`irq`] for what each trit means. Zero for anything with no devices
311    /// attached.
312    fn interrupts(&self) -> Word {
313        Word::ZERO
314    }
315}
316
317/// Interrupt request lines: which trit of the interrupt masks each device owns.
318///
319/// An interrupt request, conventionally shortened to IRQ, is a device asking
320/// the processor for attention. Ternaria gives each device one trit, in two
321/// control registers the processor holds:
322///
323/// - `ip`, interrupt pending. Written by the hardware. Trit `k` is positive
324///   while device `k` is asking. A device keeps asking until something attends
325///   to it, so the trit stays positive; it is a level, not a pulse.
326/// - `ie`, interrupt enable. Written by the operating system. Trit `k` is
327///   positive while the operating system is willing to be interrupted by
328///   device `k`.
329///
330/// A source fires when both trits are positive, which is the per-trit minimum
331/// of the two masks: Kleene AND (D-06).
332///
333/// # Why the numbering lives here
334///
335/// A bus fills this mask in and a processor reads it, and the two are separate
336/// crates that must agree on what each trit means. Neither can own the
337/// numbering without the other duplicating it, so it belongs to the trait they
338/// meet at, next to [`Addressable::interrupts`].
339pub mod irq {
340    /// The timer reached its compare value.
341    pub const TIMER: usize = 0;
342    /// The console has input waiting.
343    pub const CONSOLE: usize = 1;
344    /// How many sources are defined.
345    pub const COUNT: usize = 2;
346
347    /// The value that enables one source when written to `ie`.
348    ///
349    /// A power of three, not of two, because the position is a trit position:
350    /// enabling source `k` means putting a `+1` in trit `k`.
351    pub const fn mask(source: usize) -> i64 {
352        3i64.pow(source as u32)
353    }
354}
355
356impl Addressable for Memory {
357    fn read_tryte(&mut self, addr: Word) -> Result<Tryte, MemoryError> {
358        Memory::read_tryte(self, addr)
359    }
360    fn write_tryte(&mut self, addr: Word, value: Tryte) -> Result<(), MemoryError> {
361        Memory::write_tryte(self, addr, value)
362    }
363    fn read_word(&mut self, addr: Word) -> Result<Word, MemoryError> {
364        Memory::read_word(self, addr)
365    }
366    fn write_word(&mut self, addr: Word, value: Word) -> Result<(), MemoryError> {
367        Memory::write_word(self, addr, value)
368    }
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374    use ternaria_arith::Trit;
375
376    fn w(v: i64) -> Word {
377        Word::from_value(v)
378    }
379
380    #[test]
381    fn nat_tryte_is_all_poison() {
382        // Every one of the nine trit slots must carry the NaT pattern, or
383        // fresh memory would decode as a legitimate value somewhere.
384        for i in 0..9 {
385            assert_eq!((NAT_TRYTE >> (2 * i)) & 0b11, Trit::NAT_CODE as u32);
386        }
387        assert!(Tryte::from_bct(NAT_TRYTE).is_err());
388    }
389
390    #[test]
391    fn fresh_memory_is_uninitialised_not_zero() {
392        let m = Memory::new();
393        for addr in [0i64, 1, -1, 1_000_000, -1_000_000] {
394            assert_eq!(
395                m.read_tryte(w(addr)),
396                Err(MemoryError::Uninitialised { addr }),
397                "addr {addr}"
398            );
399        }
400        assert_eq!(m.pages_allocated(), 0, "reads must not allocate");
401    }
402
403    #[test]
404    fn tryte_round_trips_including_negative_addresses() {
405        let mut m = Memory::new();
406        for addr in [0i64, 1, 728, 729, 730, -1, -728, -729, -730, 5_000_000] {
407            for v in [0i32, 1, -1, 9841, -9841, 4242] {
408                m.write_tryte(w(addr), Tryte::from_value(v)).unwrap();
409                assert_eq!(
410                    m.read_tryte(w(addr)).unwrap().value(),
411                    v,
412                    "addr {addr} v {v}"
413                );
414            }
415        }
416    }
417
418    /// Euclidean splitting is the whole reason negative addresses work. A
419    /// truncating split would give negative offsets and index out of bounds.
420    #[test]
421    fn negative_addresses_split_into_valid_offsets() {
422        for addr in [-1i64, -728, -729, -730, -1_000_000, 0, 1, 728, 729] {
423            let (_page, offset) = Memory::split(addr);
424            assert!(offset < PAGE_TRYTES, "addr {addr} gave offset {offset}");
425        }
426        // Adjacent addresses across a page boundary must land in adjacent pages.
427        assert_eq!(Memory::split(-1), (-1, PAGE_TRYTES - 1));
428        assert_eq!(Memory::split(0), (0, 0));
429    }
430
431    #[test]
432    fn word_round_trips() {
433        let mut m = Memory::new();
434        for v in [
435            0i64,
436            1,
437            -1,
438            42,
439            -42,
440            Word::MAX.value(),
441            Word::MIN.value(),
442            1_234_567_890,
443        ] {
444            m.write_word(w(0), w(v)).unwrap();
445            assert_eq!(m.read_word(w(0)).unwrap().value(), v, "value {v}");
446        }
447    }
448
449    #[test]
450    fn word_is_little_endian() {
451        let mut m = Memory::new();
452        // A tryte is 9 trits, so a word's three trytes carry trits 0..9, 9..18
453        // and 18..27 - place values 3^0, 3^9 and 3^18. Writing exactly one of
454        // those powers puts a single +1 in exactly one tryte and zero in the
455        // others, which is what makes the placement observable at all.
456        //
457        // The low and high cases are the ones that discriminate: under
458        // big-endian they would swap. A middle-tryte value proves nothing on
459        // its own, since three trytes are symmetric about their centre.
460        for (value, expected) in [
461            (1i64, [1i32, 0, 0]),      // 3^0  -> lowest address
462            (3i64.pow(9), [0, 1, 0]),  // 3^9  -> middle
463            (3i64.pow(18), [0, 0, 1]), // 3^18 -> highest address
464        ] {
465            m.write_word(w(0), w(value)).unwrap();
466            for (i, want) in expected.iter().enumerate() {
467                assert_eq!(
468                    m.read_tryte(w(i as i64)).unwrap().value(),
469                    *want,
470                    "value {value}, tryte {i}"
471                );
472            }
473        }
474    }
475
476    /// The invariant that makes collapsing `NatError` into `Uninitialised`
477    /// exact rather than lossy: no write can ever store a NaT code, so NaT in
478    /// memory means "never written" and nothing else.
479    #[test]
480    fn no_write_can_produce_nat() {
481        for v in Tryte::MIN.value()..=Tryte::MAX.value() {
482            let bits = Tryte::from_value(v).to_bct();
483            for i in 0..9 {
484                assert_ne!(
485                    (bits >> (2 * i)) & 0b11,
486                    Trit::NAT_CODE as u32,
487                    "value {v} produced NaT at trit {i}"
488                );
489            }
490        }
491    }
492
493    #[test]
494    fn overflow_reports_how_far_it_overshot() {
495        let mut m = Memory::new();
496        let top = Word::MAX.value();
497        let err = m.write_word(w(top - 1), w(0)).unwrap_err();
498        // Needs trytes top-1, top, top+1 - one past the end.
499        assert_eq!(err.overshoot(), Some(1));
500        assert!(err.to_string().contains("by 1 tryte"));
501
502        // A double at the same place overshoots by four.
503        let err = m.write_double(w(top - 1), DoubleWord::ZERO).unwrap_err();
504        assert_eq!(err.overshoot(), Some(4));
505
506        // The other variants do not overshoot anything.
507        assert_eq!(MemoryError::Misaligned { addr: 1 }.overshoot(), None);
508        assert_eq!(MemoryError::Uninitialised { addr: 0 }.overshoot(), None);
509    }
510
511    #[test]
512    fn double_round_trips_and_aligns_to_the_word() {
513        let mut m = Memory::new();
514        for v in [
515            0i128,
516            1,
517            -1,
518            DoubleWord::MAX.value(),
519            DoubleWord::MIN.value(),
520        ] {
521            m.write_double(w(0), DoubleWord::from_value(v)).unwrap();
522            assert_eq!(m.read_double(w(0)).unwrap().value(), v);
523        }
524        // Address 3 is word-aligned but not 6-aligned. It must still work:
525        // doubles align to the word, not to their own width.
526        m.write_double(w(3), DoubleWord::from_value(-777)).unwrap();
527        assert_eq!(m.read_double(w(3)).unwrap().value(), -777);
528    }
529
530    #[test]
531    fn misalignment_traps() {
532        let mut m = Memory::new();
533        for addr in [1i64, 2, 4, 5, -1, -2] {
534            assert_eq!(
535                m.write_word(w(addr), w(0)),
536                Err(MemoryError::Misaligned { addr })
537            );
538            assert_eq!(m.read_word(w(addr)), Err(MemoryError::Misaligned { addr }));
539        }
540        // Multiples of three are fine in both directions.
541        for addr in [0i64, 3, -3, 729, -729] {
542            m.write_word(w(addr), w(1)).unwrap();
543        }
544    }
545
546    #[test]
547    fn partially_written_word_still_faults() {
548        let mut m = Memory::new();
549        // Write only the low tryte of a word; the other two stay poisoned.
550        m.write_tryte(w(0), Tryte::from_value(5)).unwrap();
551        assert_eq!(
552            m.read_word(w(0)),
553            Err(MemoryError::Uninitialised { addr: 1 })
554        );
555    }
556
557    #[test]
558    fn allocation_is_sparse() {
559        let mut m = Memory::new();
560        // Touch addresses far apart; only the touched pages should exist.
561        m.write_tryte(w(0), Tryte::ZERO).unwrap();
562        m.write_tryte(w(1_000_000_000), Tryte::ZERO).unwrap();
563        m.write_tryte(w(-1_000_000_000), Tryte::ZERO).unwrap();
564        assert_eq!(m.pages_allocated(), 3);
565
566        // Filling one page must not allocate more.
567        for i in 0..PAGE_TRYTES as i64 {
568            m.write_tryte(w(i), Tryte::ZERO).unwrap();
569        }
570        assert_eq!(m.pages_allocated(), 3);
571    }
572
573    #[test]
574    fn access_at_the_edge_of_the_address_space() {
575        let mut m = Memory::new();
576        let top = Word::MAX.value();
577
578        // MAX itself sits one past an aligned address, so MAX-1 is aligned.
579        // Alignment therefore passes and the span check is what has to catch
580        // it: a word there would need a tryte one past the top of the space.
581        assert_eq!(top.rem_euclid(WORD_TRYTES), 1);
582        let over = MemoryError::AddressOverflow {
583            addr: top - 1,
584            len: WORD_TRYTES,
585        };
586        assert_eq!(m.write_word(w(top - 1), w(1)), Err(over));
587        assert_eq!(m.read_word(w(top - 1)), Err(over));
588
589        // The highest aligned address that does fit a whole word works.
590        let highest = top - 4;
591        assert_eq!(highest.rem_euclid(WORD_TRYTES), 0);
592        assert!(highest + WORD_TRYTES - 1 <= top);
593        m.write_word(w(highest), w(-12_345)).unwrap();
594        assert_eq!(m.read_word(w(highest)).unwrap().value(), -12_345);
595
596        // And the same at the bottom of the space, which is where a truncating
597        // address split would have gone wrong.
598        let bottom = Word::MIN.value() + 1;
599        assert_eq!(bottom.rem_euclid(WORD_TRYTES), 0);
600        m.write_word(w(bottom), w(999)).unwrap();
601        assert_eq!(m.read_word(w(bottom)).unwrap().value(), 999);
602
603        // A double needs six trytes, so it runs out of room sooner than a word
604        // does at the same address.
605        assert_eq!(
606            m.write_double(w(highest), DoubleWord::ZERO),
607            Err(MemoryError::AddressOverflow {
608                addr: highest,
609                len: DOUBLE_TRYTES
610            })
611        );
612    }
613}