arnaucube
arnaucube/rv32-rsPublic Self-hosted repository
rv32-rs/src/lib.rs
src/lib.rsRaw
  1//! Implementation of the RV32I ISA. Each opcode has a comment explaining it.
  2use anyhow::{anyhow, Result};
  3
  4pub struct RV32 {
  5    regs: [u32; 32], // registers
  6    mem: Vec<u8>,    // memory
  7    pc: u32,
  8}
  9
 10impl RV32 {
 11    pub fn new(mem: Vec<u8>) -> Self {
 12        Self {
 13            regs: [0; 32],
 14            pc: 0,
 15            mem,
 16        }
 17    }
 18    pub fn set_register(&mut self, index: usize, value: u32) {
 19        assert!(index < 32, "register index must <32");
 20        if index != 0 {
 21            self.regs[index] = value;
 22        }
 23    }
 24    // validate the access
 25    fn range(&self, address: u32, size: usize) -> Result<std::ops::Range<usize>> {
 26        if !address.is_multiple_of(size as u32) {
 27            return Err(anyhow!("misaligned at address: {}", address));
 28        }
 29        let start = address as usize;
 30        let end = start
 31            .checked_add(size)
 32            .filter(|&end| end <= self.mem.len())
 33            .ok_or(anyhow!("access fault, address: {}", address))?;
 34        Ok(start..end)
 35    }
 36    fn read(&self, address: u32, size: usize) -> Result<u32> {
 37        let range = self.range(address, size)?;
 38        let mut bytes = [0; 4];
 39        bytes[..size].copy_from_slice(&self.mem[range]);
 40        Ok(u32::from_le_bytes(bytes))
 41    }
 42    pub fn step(&mut self) -> Result<()> {
 43        let instruction = self.read(self.pc, 4)?;
 44        let mut next_pc = self.pc.wrapping_add(4);
 45
 46        // R-type instruction:
 47        //                             R-type:
 48        // 31        25 24        20 19        15 14    12 11     7 6        0
 49        // ┌───────────┬────────────┬────────────┬────────┬────────┬─────────┐
 50        // │   funct7  │    rs2     │    rs1     │ funct3 │   rd   │ opcode  │
 51        // └───────────┴────────────┴────────────┴────────┴────────┴─────────┘
 52        let rd = ((instruction >> 7) & 31) as usize;
 53        let funct3 = (instruction >> 12) & 7;
 54        let rs1 = ((instruction >> 15) & 31) as usize;
 55        let rs2 = ((instruction >> 20) & 31) as usize;
 56        let funct7 = instruction >> 25;
 57        //                             I-type:
 58        // 31                      20 19        15 14    12 11    7 6        0
 59        // ┌────────────────────────┬────────────┬────────┬────────┬─────────┐
 60        // │     imm[11:0]          │    rs1     │ funct3 │   rd   │ opcode  │
 61        // └────────────────────────┴────────────┴────────┴────────┴─────────┘
 62        //                             S-type:
 63        // 31        25 24        20 19        15 14    12 11     7 6        0
 64        // ┌───────────┬────────────┬────────────┬────────┬────────┬─────────┐
 65        // │ imm[11:5] │    rs2     │    rs1     │ funct3 │imm[4:0]│ opcode  │
 66        // └───────────┴────────────┴────────────┴────────┴────────┴─────────┘
 67        //                             B-type:
 68        // 31    30       25 24   20 19        15 14    12 11     8 7 6      0
 69        // ┌─────┬─────────┬────────┬────────────┬────────┬────────┬──┬──────┐
 70        // │imm12│imm[10:5]│ rs2    │    rs1     │ funct3 │imm[4:1]│11│opcode│
 71        // └─────┴─────────┴────────┴────────────┴────────┴────────┴──┴──────┘
 72        //
 73        // Note: we keep `imm` as the I-type imm, and in the S-type we compute
 74        // the S-type respective imm from 31-25 and 11-7 positions, and for
 75        // B-type we obtain imm from the positions 31, 31-25, 11-8, 7, and
 76        // imm[0]=0.
 77        let imm = sign_extend(instruction >> 20, 12);
 78
 79        let a = self.regs[rs1];
 80        let b = self.regs[rs2];
 81        let mut result: Option<u32> = None;
 82
 83        // 0x7f=0111_1111, match the lowest 7 bits to read the opcode
 84        match instruction & 0x7f {
 85            // I-type op'ns, loads:
 86            0x03 => {
 87                let (size, signed) = match funct3 {
 88                    // LB: load a byte at rs1 + immediate and sign-extend it
 89                    // into rd
 90                    0 => (1, true),
 91                    // LH: load a halfword at rs1 + immediate and sign-extend it
 92                    // into rd
 93                    1 => (2, true),
 94                    // LW: load a 32-bit word at rs1 + immediate into rd
 95                    2 => (4, false),
 96                    // LBU: load a byte at rs1 + immediate and zero-extend it
 97                    // into rd
 98                    4 => (1, false),
 99                    // LHU: load a halfword at rs1 + immediate and zero-extend
100                    // it into rd
101                    5 => (2, false),
102                    _ => {
103                        return Err(anyhow!(
104                            "illegal instruction in I-type (load): {}",
105                            instruction
106                        ))
107                    }
108                };
109                let value = self.read(a.wrapping_add(imm), size)?;
110                result = Some(if signed {
111                    sign_extend(value, size as u32 * 8)
112                } else {
113                    value
114                });
115            }
116            // I-type arithmetic & logical & shifts op'ns:
117            0x13 => {
118                result = Some(match funct3 {
119                    // ADDI: add the sign-extended immediate to rs1, wrapping to
120                    // 32 bits
121                    0 => a.wrapping_add(imm),
122                    // SLTI: signed comparison. Set rd to one if rs1 is less
123                    // than the immediate as signed values
124                    2 => u32::from((a as i32) < (imm as i32)),
125                    // SLTIU: unsigned comparison. Compare rs1 and the
126                    // sign-extended immediate as unsigned values
127                    3 => u32::from(a < imm),
128                    // XORI: write the bitwise XOR of rs1 and the sign-extended
129                    // immediate
130                    4 => a ^ imm,
131                    // ORI: write the bitwise OR of rs1 and the sign-extended
132                    // immediate
133                    6 => a | imm,
134                    // ANDI: write the bitwise AND of rs1 and the sign-extended
135                    // immediate
136                    7 => a & imm,
137                    // SLLI: shift rs1 left by the five-bit immediate shift
138                    // amount
139                    1 if funct7 == 0 => a << rs2,
140                    // SRLI: logical right shift. Shift rs1 right by the
141                    // immediate amount, filling with zeros
142                    5 if funct7 == 0 => a >> rs2,
143                    // SRAI: arithmetic right shift. Shift rs1 right by the
144                    // immediate amount, preserving its sign
145                    5 if funct7 == 0x20 => ((a as i32) >> rs2) as u32,
146                    _ => {
147                        return Err(anyhow!(
148                            "illegal instruction in I-type (arithmetic/logical): {}",
149                            instruction
150                        ))
151                    }
152                });
153            }
154            // S-type op'ns:
155            0x23 => {
156                let size = match funct3 {
157                    // SB: store the low byte of rs2 at rs1 + immediate
158                    0 => 1,
159                    // SH: store the low halfword of rs2 at rs1 + immediate
160                    1 => 2,
161                    // SW: store the full 32-bit value of rs2 at rs1 + immediate
162                    2 => 4,
163                    _ => return Err(anyhow!("illegal instruction in S-type: {}", instruction)),
164                };
165                // for the S-type imm, get it from 25-31 and 7-11 positions
166                let s_type_imm = ((instruction >> 25) << 5) | ((instruction >> 7) & 31);
167                let offset = sign_extend(s_type_imm, 12);
168                let range = self.range(a.wrapping_add(offset), size)?;
169                self.mem[range].copy_from_slice(&b.to_le_bytes()[..size]);
170            }
171
172            // R-type (register-register) integer operations
173            0x33 => {
174                result = Some(match (funct7, funct3) {
175                    // ADD: add rs1 and rs2, wrapping to 32 bits
176                    (0, 0) => a.wrapping_add(b),
177                    // SUB: subtract rs2 from rs1, wrapping to 32 bits
178                    (0x20, 0) => a.wrapping_sub(b),
179                    // SLL: shift rs1 left by the low five bits of rs2
180                    (0, 1) => a << (b & 31),
181                    // SLT: set rd to one if rs1 is less than rs2 as signed
182                    // values, else zero
183                    (0, 2) => u32::from((a as i32) < (b as i32)),
184                    // SLTU: set rd to one if rs1 is less than rs2 as unsigned
185                    // values, else zero
186                    (0, 3) => u32::from(a < b),
187                    // XOR: write the bitwise XOR of rs1 and rs2 into rd
188                    (0, 4) => a ^ b,
189                    // SRL: shift rs1 right by the low five bits of rs2, filling
190                    // with zeros
191                    (0, 5) => a >> (b & 31),
192                    // SRA: shift rs1 right by the low five bits of rs2,
193                    // preserving its sign
194                    (0x20, 5) => ((a as i32) >> (b & 31)) as u32,
195                    // OR: write the bitwise OR of rs1 and rs2 into rd
196                    (0, 6) => a | b,
197                    // AND: write the bitwise AND of rs1 and rs2 into rd
198                    (0, 7) => a & b,
199
200                    // Note: MUL,DIV,REM are not part of RV32I, but they are in
201                    // the M extension.
202
203                    // MUL: write the low 32 bits of the product of rs1 and rs2
204                    (1, 0) => a.wrapping_mul(b),
205                    // DIV: signed quotient toward zero; zero divisors return
206                    // all ones
207                    (1, 4) => {
208                        if b == 0 {
209                            u32::MAX
210                        } else {
211                            (a as i32).wrapping_div(b as i32) as u32
212                        }
213                    }
214                    // REM: signed remainder; zero divisors return the dividend
215                    (1, 6) => {
216                        if b == 0 {
217                            a
218                        } else {
219                            (a as i32).wrapping_rem(b as i32) as u32
220                        }
221                    }
222
223                    _ => return Err(anyhow!("illegal instruction in R-type: {}", instruction)),
224                });
225            }
226            // B-type operations
227            0x63 => {
228                let taken = match funct3 {
229                    // BEQ: branch by the signed PC-relative offset if the
230                    // registers are equal
231                    0 => a == b,
232                    // BNE: branch if the registers are unequal
233                    1 => a != b,
234                    // BLT: branch if rs1 is less than rs2 using signed comparison
235                    4 => (a as i32) < (b as i32),
236                    // BGE: branch if rs1 is at least rs2 using signed comparison
237                    5 => (a as i32) >= (b as i32),
238                    // BLTU: branch if rs1 is less than rs2 using unsigned comparison
239                    6 => a < b,
240                    // BGEU: branch if rs1 is at least rs2 using unsigned comparison
241                    7 => a >= b,
242                    _ => return Err(anyhow!("illegal instruction in B-type: {}", instruction)),
243                };
244                if taken {
245                    // offset: reconstruct imm for B-type from positions 31,
246                    // 31-25, 11-8, 7, and imm[0]=0
247                    let b_type_imm = ((instruction >> 31) << 12)
248                        | (((instruction >> 7) & 1) << 11)
249                        | (((instruction >> 25) & 0x3f) << 5)
250                        | (((instruction >> 8) & 15) << 1);
251                    next_pc = self.pc.wrapping_add(sign_extend(b_type_imm, 13));
252                }
253            }
254
255            _ => return Err(anyhow!("illegal instruction: {}", instruction)),
256        }
257
258        if next_pc & 3 != 0 {
259            return Err(anyhow!("misaligned, address: {}", next_pc,));
260        }
261        if let Some(value) = result {
262            self.set_register(rd, value);
263        }
264        self.pc = next_pc;
265        Ok(())
266    }
267}
268
269// sign-extend a smaller signed number to 32 bits
270fn sign_extend(value: u32, bits: u32) -> u32 {
271    ((value << (32 - bits)) as i32 >> (32 - bits)) as u32
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    const OPCODE_R: u32 = 0b0110011;
279    const OPCODE_LOAD: u32 = 0b0000011;
280
281    // encodes R-type instruction
282    fn encode_r(name: &str, rd: u32, rs1: u32, rs2: u32) -> u32 {
283        let (funct7, funct3) = match name {
284            "add" => (0b0000000, 0b000),
285            "sub" => (0b0100000, 0b000),
286            "sll" => (0b0000000, 0b001),
287            "slt" => (0b0000000, 0b010),
288            "sltu" => (0b0000000, 0b011),
289            "xor" => (0b0000000, 0b100),
290            "srl" => (0b0000000, 0b101),
291            "sra" => (0b0100000, 0b101),
292            "or" => (0b0000000, 0b110),
293            "and" => (0b0000000, 0b111),
294            // M extension
295            "mul" => (0b0000001, 0b000),
296            "div" => (0b0000001, 0b100),
297            "rem" => (0b0000001, 0b110),
298            _ => panic!("unknown R-type instruction: {name}"),
299        };
300
301        (funct7 << 25) | (rs2 << 20) | (rs1 << 15) | (funct3 << 12) | (rd << 7) | OPCODE_R
302    }
303    fn encode_i(name: &str, rd: u32, rs1: u32, imm: i32) -> u32 {
304        let (funct3, opcode) = match name {
305            // loads: opcode = 0x03
306            "lb" => (0b000, OPCODE_LOAD),
307            "lh" => (0b001, OPCODE_LOAD),
308            "lw" => (0b010, OPCODE_LOAD),
309            "lbu" => (0b100, OPCODE_LOAD),
310            "lhu" => (0b101, OPCODE_LOAD),
311            // arithmetic & logic & shifts: opcode = 0x13
312            "addi" => (0b000, 0x13),
313            "slti" => (0b010, 0x13),
314            "sltiu" => (0b011, 0x13),
315            "xori" => (0b100, 0x13),
316            "ori" => (0b110, 0x13),
317            "andi" => (0b111, 0x13),
318            "slli" => (0b001, 0x13),
319            "srli" => (0b101, 0x13),
320            "srai" => (0b101, 0x13),
321
322            _ => panic!("unknown I-type instruction: {name}"),
323        };
324        let funct7 = if name == "srai" {
325            0b010_0000
326        } else {
327            0b000_0000
328        };
329
330        let imm = (imm as u32) & 0xfff;
331
332        (imm << 20) | (funct7 << 25) | (rs1 << 15) | (funct3 << 12) | (rd << 7) | opcode
333    }
334    fn encode_s(name: &str, rs1: u32, rs2: u32, imm: i32) -> u32 {
335        let (opcode, funct3) = match name {
336            "sb" => (0b0100011, 0b000),
337            "sh" => (0b0100011, 0b001),
338            "sw" => (0b0100011, 0b010),
339            _ => panic!("unknown store instruction: {name}"),
340        };
341
342        let imm = imm as u32;
343
344        let imm_11_5 = (imm >> 5) & 0x7f;
345        let imm_4_0 = imm & 0x1f;
346
347        (imm_11_5 << 25) | (rs2 << 20) | (rs1 << 15) | (funct3 << 12) | (imm_4_0 << 7) | opcode
348    }
349    fn encode_b(name: &str, rs1: u32, rs2: u32, offset: i32) -> u32 {
350        let funct3 = match name {
351            "beq" => 0b000,
352            "bne" => 0b001,
353            "blt" => 0b100,
354            "bge" => 0b101,
355            "bltu" => 0b110,
356            "bgeu" => 0b111,
357            _ => panic!("unknown B-type instruction: {name}"),
358        };
359
360        assert!(
361            offset % 2 == 0,
362            "B-type branch offset must be 2-byte aligned"
363        );
364
365        assert!(
366            (-4096..=4094).contains(&offset),
367            "B-type branch offset out of range: {offset}"
368        );
369
370        let imm = offset as u32;
371
372        let imm12 = (imm >> 12) & 0x1;
373        let imm11 = (imm >> 11) & 0x1;
374        let imm10_5 = (imm >> 5) & 0x3f;
375        let imm4_1 = (imm >> 1) & 0xf;
376
377        (imm12 << 31)
378            | (imm10_5 << 25)
379            | (rs2 << 20)
380            | (rs1 << 15)
381            | (funct3 << 12)
382            | (imm4_1 << 8)
383            | (imm11 << 7)
384            | 0b1100011
385    }
386
387    // R-type ops test
388    #[test]
389    fn test_ops_r_type() {
390        let test_vectors = vec![
391            // (op, rs1, rs2, rd)
392            ("add", 10, 20, 30),
393            ("add", u32::MAX, 1, 0), // overflow
394            ("sub", 30, 20, 10),
395            ("sub", 0, 1, u32::MAX), // overflow
396            ("sll", 1, 4, 16),
397            ("sll", 1, 31, 0x8000_0000), // SLL maximum shift
398            ("sll", 1, 32, 1),           // SLL shift wrapping
399            ("slt", (-10i32) as u32, 20, 1),
400            ("sltu", 10, 20, 1),
401            ("slt", 0xffff_ffff, 0, 1), // signed comparison: -1 < 0
402            ("slt", 0xffff_ffff, 0, 1), // signed vs unsigned distinction
403            ("sltu", 0xffff_ffff, 0, 0),
404            ("xor", 0b1010, 0b1100, 0b0110),
405            ("srl", 0x8000_0000, 4, 0x0800_0000),
406            ("srl", 0x8000_0000, 1, 0x4000_0000), // logical shift should zero-fill
407            ("sra", 0x8000_0000, 4, 0xf800_0000),
408            ("sra", 0x8000_0000, 1, 0xc000_0000), // arithmetic shift should sign-fill
409            ("sra", 0xffff_ffff, 31, 0xffff_ffff), // arithmetic shift of -1 stays -1
410            ("or", 0b1010, 0b1100, 0b1110),
411            ("and", 0b1010, 0b1100, 0b1000),
412        ];
413        test_ops_r_types_opt(test_vectors);
414    }
415    #[test]
416    fn test_ops_m_extension() {
417        let test_vectors = vec![
418            ("mul", 10, 20, 200),
419            ("mul", 0, 123, 0),
420            ("mul", 1, u32::MAX, u32::MAX),
421            ("mul", u32::MAX, 2, u32::MAX - 1), // result wraps to lower 32 bits
422            ("mul", (-1i32) as u32, 10, (-10i32) as u32), // -1 * 10 = -10
423            ("mul", (-1i32) as u32, (-1i32) as u32, 1), // -1 * -1 = 1
424            ("mul", 0x8000_0000, 2, 0),         // INT_MIN * 2 = 0 modulo 2^32
425            ("mul", 0x8000_0000, 0xffff_ffff, 0x8000_0000), // INT_MIN * -1 also wraps to INT_MIN
426            ("div", 20, 5, 4),
427            ("div", 21, 5, 4), // truncates toward zero
428            ("div", 0, 10, 0),
429            ("div", 10, 1, 10),
430            ("div", 10, (-1i32) as u32, (-10i32) as u32),
431            ("div", (-20i32) as u32, 5, (-4i32) as u32), // negative dividend
432            ("div", 20, (-5i32) as u32, (-4i32) as u32), // negative divisor
433            ("div", (-20i32) as u32, (-5i32) as u32, 4), // negative / negative
434            ("div", (-21i32) as u32, 5, (-4i32) as u32), // truncation toward zero: -21 / 5 = -4
435            // RISC-V special case:
436            ("div", 0x8000_0000, 0xffff_ffff, 0x8000_0000), // INT_MIN / -1 = INT_MIN (overflow)
437            ("div", 10, 0, u32::MAX), // division by zero returns -1 for signed DIV
438            ("div", 0x8000_0000, 0, u32::MAX),
439            ("rem", 20, 5, 0),
440            ("rem", 21, 5, 1),
441            ("rem", 0, 10, 0),
442            ("rem", 10, 1, 0),
443            ("rem", 23, 5, 3),                           // positive remainder
444            ("rem", (-23i32) as u32, 5, (-3i32) as u32), // remainder follows the sign of the dividend
445            ("rem", 23, (-5i32) as u32, 3), // negative divisor does not change the sign of the remainder
446            ("rem", (-23i32) as u32, (-5i32) as u32, (-3i32) as u32), // negative / negative
447            ("rem", 3, 5, 3),               // dividend smaller than divisor
448            // RISC-V special case:
449            ("rem", 0x8000_0000, 0xffff_ffff, 0), // INT_MIN % -1 = 0.
450            ("rem", 123, 0, 123),                 // remainder by zero returns the dividend.
451            ("rem", u32::MAX, 0, u32::MAX),
452            ("rem", 0x8000_0000, 0, 0x8000_0000),
453        ];
454        test_ops_r_types_opt(test_vectors);
455    }
456    fn test_ops_r_types_opt(test_vectors: Vec<(&str, u32, u32, u32)>) {
457        for (name, rs1_value, rs2_value, expected) in test_vectors {
458            let rd = 3;
459            let rs1 = 1;
460            let rs2 = 2;
461
462            let instruction = encode_r(name, rd, rs1, rs2);
463
464            let mut cpu = RV32::new(instruction.to_le_bytes().to_vec());
465
466            cpu.regs[rs1 as usize] = rs1_value;
467            cpu.regs[rs2 as usize] = rs2_value;
468
469            cpu.step().unwrap();
470
471            assert_eq!(
472                cpu.regs[rd as usize], expected,
473                "{name} failed:
474                 rs1={rs1_value:#010x}, rs2={rs2_value:#010x}, 
475                 expected={expected:#010x}, got={:#010x}",
476                cpu.regs[rd as usize],
477            );
478        }
479    }
480
481    // I-type ops test
482    #[test]
483    fn test_ops_load() {
484        let test_vectors: Vec<(&str, u32, i32, u32)> = vec![
485            ("lb", 0x0000_0080, 0, 0xffff_ff80),
486            ("lbu", 0x0000_0080, 0, 0x0000_0080),
487            ("lb", 0x0000_00ff, 0, 0xffff_ffff),
488            ("lbu", 0x0000_00ff, 0, 0x0000_00ff),
489            ("lh", 0x0000_8000, 0, 0xffff_8000),
490            ("lhu", 0x0000_8000, 0, 0x0000_8000),
491            ("lh", 0x0000_ffff, 0, 0xffff_ffff),
492            ("lhu", 0x0000_ffff, 0, 0x0000_ffff),
493        ];
494
495        for (name, memory_value, address, expected) in test_vectors {
496            let rd = 1;
497            let rs1 = 2;
498
499            let instruction = encode_i(name, rd, rs1, address);
500
501            let mut memory = instruction.to_le_bytes().to_vec();
502            let data_address = memory.len() as u32;
503            memory.extend_from_slice(&memory_value.to_le_bytes());
504            let mut cpu = RV32::new(memory);
505
506            cpu.regs[rs1 as usize] = data_address;
507
508            cpu.step().unwrap();
509
510            assert_eq!(
511                cpu.regs[rd as usize], expected,
512                "{name} failed: memory={memory_value:#010x},
513                 address={address}, \
514                 expected={expected:#010x},
515                 got={:#010x}",
516                cpu.regs[rd as usize],
517            );
518        }
519    }
520    #[test]
521    fn test_ops_i_type_arithmetic() {
522        // almost same test vectors as for `test_ops_r_type`, same names with
523        // `i` as suffix
524        let test_vectors: Vec<(&str, u32, i32, u32)> = vec![
525            // (name, rs1, immediate, expected rd)
526            ("addi", 10, 20, 30),
527            ("addi", u32::MAX, 1, 0), // overflow
528            ("slli", 1, 4, 16),
529            ("slli", 1, 31, 0x8000_0000), // SLL maximum shift
530            ("slti", (-10i32) as u32, 20, 1),
531            ("sltiu", 10, 20, 1),
532            ("slti", 0xffff_ffff, 0, 1), // signed comparison: -1 < 0
533            ("slti", 0xffff_ffff, 0, 1), // signed vs unsigned distinction
534            ("sltiu", 0xffff_ffff, 0, 0),
535            ("xori", 0b1010, 0b1100, 0b0110),
536            ("srli", 0x8000_0000, 4, 0x0800_0000),
537            ("srli", 0x8000_0000, 1, 0x4000_0000), // logical shift should zero-fill
538            ("srai", 0x8000_0000, 4, 0xf800_0000),
539            ("srai", 0x8000_0000, 1, 0xc000_0000), // arithmetic shift should sign-fill
540            ("srai", 0xffff_ffff, 31, 0xffff_ffff), // arithmetic shift of -1 stays -1
541            ("ori", 0b1010, 0b1100, 0b1110),
542            ("andi", 0b1010, 0b1100, 0b1000),
543        ];
544
545        for (name, rs1_value, imm, expected) in test_vectors {
546            dbg!(name);
547            let rd = 1;
548            let rs1 = 2;
549
550            let instruction = encode_i(name, rd, rs1, imm);
551            let mut cpu = RV32::new(instruction.to_le_bytes().to_vec());
552
553            cpu.regs[rs1 as usize] = rs1_value;
554
555            cpu.step().unwrap();
556
557            assert_eq!(
558                cpu.regs[rd as usize], expected,
559                "{name} failed: rs1={rs1_value:#010x}, \
560             imm={imm}, \
561             expected={expected:#010x}, \
562             got={:#010x}",
563                cpu.regs[rd as usize],
564            );
565        }
566    }
567
568    // S-type ops test
569    #[test]
570    fn test_ops_store() {
571        let test_vectors: Vec<(&str, u32, i32)> = vec![
572            ("sb", 0x0000_0080, 0),
573            ("sb", 0x0000_00ff, 0),
574            ("sb", 0x1234_5678, 0),
575            ("sh", 0x0000_8000, 0),
576            ("sh", 0x0000_ffff, 0),
577            ("sw", 0x1234_5678, 0),
578        ];
579
580        for (name, value, address) in test_vectors {
581            let rs1 = 1;
582            let rs2 = 2;
583
584            let instruction = encode_s(name, rs1, rs2, address);
585
586            let mut memory = instruction.to_le_bytes().to_vec();
587            let data_address = memory.len() as u32;
588
589            // add space for the value being stored
590            memory.extend_from_slice(&[0u8; 4]);
591
592            let mut cpu = RV32::new(memory);
593
594            cpu.regs[rs1 as usize] = data_address;
595            cpu.regs[rs2 as usize] = value;
596
597            cpu.step().unwrap();
598
599            let stored = u32::from_le_bytes(
600                cpu.mem[data_address as usize..data_address as usize + 4]
601                    .try_into()
602                    .unwrap(),
603            );
604
605            let expected = match name {
606                "sb" => value & 0xff,
607                "sh" => value & 0xffff,
608                "sw" => value,
609                _ => unreachable!(),
610            };
611
612            assert_eq!(
613                stored, expected,
614                "{name} failed: value={value:#010x},
615                 address={address}, \
616                 expected={expected:#010x},
617                 got={stored:#010x}",
618            );
619        }
620    }
621
622    // B-type ops test
623    #[test]
624    fn test_ops_branch() {
625        let test_vectors: Vec<(&str, u32, u32, i32, bool)> = vec![
626            // (name, rs1 value, rs2 value, offset, taken)
627            ("beq", 10, 10, 8, true),
628            ("beq", 10, 20, 8, false),
629            ("bne", 10, 20, 8, true),
630            ("bne", 10, 10, 8, false),
631            ("blt", 10, 20, 8, true),
632            ("blt", 20, 10, 8, false),
633            ("bge", 20, 10, 8, true),
634            ("bge", 10, 20, 8, false),
635            ("bltu", 10, 20, 8, true),
636            ("bltu", 20, 10, 8, false),
637            ("bgeu", 20, 10, 8, true),
638            ("bgeu", 10, 20, 8, false),
639            ("beq", 10, 10, -4, true), // negative branch offsets
640            ("bne", 10, 20, -4, true),
641            ("blt", 0xffff_ffff, 0, 8, true), // signed vs unsigned comparison
642            ("bltu", 0xffff_ffff, 0, 8, false),
643            ("bge", 0xffff_ffff, 0, 8, false),
644            ("bgeu", 0xffff_ffff, 0, 8, true),
645        ];
646
647        for (name, rs1_value, rs2_value, offset, taken) in test_vectors {
648            let rs1 = 1;
649            let rs2 = 2;
650
651            let instruction = encode_b(name, rs1, rs2, offset);
652            let mut cpu = RV32::new(instruction.to_le_bytes().to_vec());
653
654            cpu.regs[rs1 as usize] = rs1_value;
655            cpu.regs[rs2 as usize] = rs2_value;
656
657            let initial_pc = cpu.pc;
658
659            cpu.step().unwrap();
660
661            let expected_pc = if taken {
662                initial_pc.wrapping_add(offset as u32)
663            } else {
664                initial_pc.wrapping_add(4)
665            };
666
667            assert_eq!(
668                cpu.pc, expected_pc,
669                "{name} failed: rs1={rs1_value:#010x}, \
670                 rs2={rs2_value:#010x}, offset={offset}, \
671                 expected_pc={expected_pc:#010x}, \
672                 got={:#010x}",
673                cpu.pc,
674            );
675        }
676    }
677}