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}