Skip to main content

rustyn64_cpu/
regs.rs

1//! The VR4300 register file.
2//!
3//! Split out so `$zero`'s hardwiring lives in exactly one place. Every read goes
4//! through [`Regs::read`] and every write through [`Regs::write`], so no call
5//! site can forget it — a scattered `if rd != 0` is how a write to `$zero`
6//! eventually slips through and corrupts the architectural zero.
7
8use serde::{Deserialize, Serialize};
9
10/// MIPS general-purpose register indices, by their ABI names.
11///
12/// Lives here rather than beside any one caller so the numbering is stated once:
13/// a second private copy elsewhere is how `$t3` and `$t4` eventually swap in one
14/// of them. Only the names currently needed are defined — this is a shared
15/// vocabulary, not an exhaustive table for its own sake.
16pub mod gpr {
17    /// `$at` — assembler temporary.
18    pub const AT: u8 = 1;
19    /// `$a2` — argument 2.
20    pub const A2: u8 = 6;
21    /// `$a3` — argument 3.
22    pub const A3: u8 = 7;
23    /// `$t0` — temporary 0.
24    pub const T0: u8 = 8;
25    /// `$t2` — temporary 2.
26    pub const T2: u8 = 10;
27    /// `$t3` — temporary 3.
28    pub const T3: u8 = 11;
29    /// `$s3` — saved 3.
30    pub const S3: u8 = 19;
31    /// `$s4` — saved 4.
32    pub const S4: u8 = 20;
33    /// `$s5` — saved 5.
34    pub const S5: u8 = 21;
35    /// `$s6` — saved 6.
36    pub const S6: u8 = 22;
37    /// `$s7` — saved 7.
38    pub const S7: u8 = 23;
39    /// `$sp` — stack pointer.
40    pub const SP: u8 = 29;
41    /// `$ra` — return address.
42    pub const RA: u8 = 31;
43}
44
45/// General-purpose registers plus the `HI`/`LO` multiply-divide pair.
46#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
47pub struct Regs {
48    /// 32 general-purpose 64-bit registers. Index 0 is architecturally zero;
49    /// prefer [`Regs::read`] / [`Regs::write`] over touching this directly.
50    pub gpr: [u64; 32],
51    /// `HI` — multiply high half, or division remainder.
52    pub hi: u64,
53    /// `LO` — multiply low half, or division quotient.
54    pub lo: u64,
55}
56
57impl Default for Regs {
58    fn default() -> Self {
59        Self::new()
60    }
61}
62
63impl Regs {
64    /// Power-on state: everything zero.
65    #[must_use]
66    pub const fn new() -> Self {
67        Self {
68            gpr: [0; 32],
69            hi: 0,
70            lo: 0,
71        }
72    }
73
74    /// Read a general register. `$zero` always reads as 0.
75    ///
76    /// The index is masked to 5 bits **first**, then the `$zero` rule is applied
77    /// to the *masked* value. Checking before masking would let `read(32)` fall
78    /// through to `gpr[32 & 31]` — `gpr[0]` — and leak it if it were ever
79    /// corrupted. This is public API and cannot assume its caller pre-masked.
80    #[must_use]
81    pub const fn read(&self, i: u8) -> u64 {
82        let i = i & 31;
83        if i == 0 { 0 } else { self.gpr[i as usize] }
84    }
85
86    /// Write a general register. A write to `$zero` is **discarded**, which is
87    /// architectural, not a convenience: software relies on `$zero` staying zero
88    /// after instructions that nominally target it.
89    ///
90    /// Masked to 5 bits **before** the `$zero` check, for the same reason as
91    /// [`Regs::read`] but with worse consequences: checking first would make
92    /// `write(32, v)` land in `gpr[0]` and corrupt the architectural zero — in
93    /// the one function whose entire purpose is preventing exactly that.
94    pub const fn write(&mut self, i: u8, v: u64) {
95        let i = i & 31;
96        if i != 0 {
97            self.gpr[i as usize] = v;
98        }
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn zero_reads_as_zero_and_cannot_be_written() {
108        let mut r = Regs::new();
109        r.write(0, 0xDEAD_BEEF);
110        assert_eq!(r.read(0), 0);
111        assert_eq!(r.gpr[0], 0, "the raw array must be untouched too");
112    }
113
114    /// An out-of-range index must not alias `$zero`. `write(32, v)` naively
115    /// masks to `gpr[0]` and corrupts the architectural zero — in the very
116    /// function that exists to prevent that.
117    #[test]
118    fn out_of_range_indices_do_not_alias_zero() {
119        let mut r = Regs::new();
120        for i in [32u8, 64, 96, 128, 160, 192, 224] {
121            r.write(i, 0xDEAD_BEEF);
122            assert_eq!(r.gpr[0], 0, "write({i}) corrupted $zero");
123            assert_eq!(r.read(i), 0, "read({i}) did not honor the $zero rule");
124        }
125        // A corrupted gpr[0] must still never be observable through `read`.
126        r.gpr[0] = 0xBAD;
127        assert_eq!(r.read(0), 0);
128        assert_eq!(r.read(32), 0);
129    }
130
131    #[test]
132    fn ordinary_registers_round_trip() {
133        let mut r = Regs::new();
134        for i in 1..32u8 {
135            r.write(i, u64::from(i) * 0x1111_1111);
136        }
137        for i in 1..32u8 {
138            assert_eq!(r.read(i), u64::from(i) * 0x1111_1111);
139        }
140    }
141}