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}