rustyn64_cpu/exec.rs
1//! `EX`-stage execution: [`Decoded`] plus operands in, result out (T-11-002).
2//!
3//! The bridge between [`mod@crate::decode`] and [`crate::alu`]. Kept separate from
4//! the pipeline so it stays a pure function of `(op, rs_val, rt_val, hi, lo)` —
5//! testable without a machine, and with no way to accidentally read register
6//! state the decode did not name.
7
8use crate::Exception;
9use crate::alu::{self, HiLo, MulDiv};
10use crate::decode::{Decoded, Op};
11use crate::mem::{LoadKind, StoreKind};
12use serde::{Deserialize, Serialize};
13
14/// What an executed instruction wants written back.
15#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
16pub enum WriteBack {
17 /// Nothing to commit.
18 #[default]
19 None,
20 /// Write `value` to general register `dest`.
21 Gpr {
22 /// Destination register index.
23 dest: u8,
24 /// Value to commit.
25 value: u64,
26 },
27 /// Write both `HI` and `LO` (every multiply and divide does).
28 HiLo(HiLo),
29 /// Write `HI` alone (`MTHI`).
30 Hi(u64),
31 /// Write `LO` alone (`MTLO`).
32 Lo(u64),
33}
34
35/// A memory access `EX` computed and `DC` must perform.
36///
37/// `EX` resolves the effective address and hands the access to `DC`; it does not
38/// touch the bus itself. That split is the point of the pipeline — `DC` is the
39/// cycle the scheduler interleaves the RCP around (ADR 0007).
40#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
41pub enum MemOp {
42 /// An aligned load into `dest`.
43 Load {
44 /// Width and signedness.
45 kind: LoadKind,
46 /// Effective address.
47 addr: u64,
48 /// Destination register.
49 dest: u8,
50 },
51 /// An aligned store of `value`.
52 Store {
53 /// Width.
54 kind: StoreKind,
55 /// Effective address.
56 addr: u64,
57 /// Value from `rt`.
58 value: u64,
59 },
60 /// A **load linked**: an aligned load that also arms the link bit and
61 /// records the physical address in `LLAddr` (UM §16 p. 453).
62 LinkedLoad {
63 /// Width and signedness.
64 kind: LoadKind,
65 /// Effective address.
66 addr: u64,
67 /// Destination register.
68 dest: u8,
69 },
70 /// A **store conditional**: stores `value` only if the link bit is set, and
71 /// writes the outcome (1 = stored, 0 = not) to `dest` either way.
72 ///
73 /// Carrying `dest` is what makes this distinct from [`MemOp::Store`] — the
74 /// flag is architecturally visible even when nothing is written to memory
75 /// (UM §16 p. 487).
76 ConditionalStore {
77 /// Width.
78 kind: StoreKind,
79 /// Effective address.
80 addr: u64,
81 /// Value from `rt`.
82 value: u64,
83 /// Destination register for the success flag.
84 dest: u8,
85 },
86 /// An **EMUX** emulator-extension operation (COP0 CO `funct` 0x20-0x3F).
87 ///
88 /// Carried to `DC` because it needs the bus: `xlog` reads a string out of
89 /// guest memory and hands it to the host. `execute` is pure and cannot.
90 Emux {
91 /// The CO `funct`: `0x20` xdetect, `0x25` xlog, `0x2C` xioctl.
92 funct: u8,
93 /// The 9-bit `code` field (bits 14:6).
94 code: u16,
95 /// `GPR[rd]` — `xlog`'s string pointer.
96 ptr: u64,
97 /// `GPR[rt]` — `xlog`'s length.
98 len: u64,
99 /// `rd`, where `xdetect` returns its capability mask.
100 dest: u8,
101 },
102 /// A `CACHE` maintenance operation.
103 ///
104 /// Carries the effective address so `DC` translates it — the instruction can
105 /// raise a TLB fault — and the 5-bit operation selector so a trace can name
106 /// what was requested. No data moves.
107 Cache {
108 /// Effective address.
109 addr: u64,
110 /// The `op` field (the instruction's `rt` slot): bits 1..=0 select the
111 /// cache, bits 4..=2 the operation.
112 op: u8,
113 },
114 /// An FP load or store (`LWC1`/`LDC1`/`SWC1`/`SDC1`).
115 ///
116 /// Kept separate from [`MemOp::Load`]/[`MemOp::Store`] because the value
117 /// moves to or from the **FP** register file, and `DC` needs to know which —
118 /// a shared variant would need a "which file" flag anyway.
119 Fp {
120 /// Which of the four forms.
121 op: Op,
122 /// Effective address.
123 addr: u64,
124 /// The FPR (`ft`).
125 ft: u8,
126 },
127 /// One half of an unaligned access. `rt` is needed for both directions: a
128 /// partial load merges into it, and a partial store merges out of it.
129 Unaligned {
130 /// Which of the eight forms.
131 op: Op,
132 /// Effective address (deliberately **not** aligned down here — `DC`
133 /// needs the low bits to know which bytes are covered).
134 addr: u64,
135 /// Current `rt`.
136 rt: u64,
137 /// Destination register, or 0 for the store forms.
138 dest: u8,
139 },
140}
141
142/// A control-flow redirect `EX` resolved.
143///
144/// The delay slot has *already been fetched* by the time `EX` resolves a branch,
145/// which is the whole point of the architectural delay slot. What `EX` decides is
146/// where the fetch *after* the delay slot goes, and whether the delay slot runs
147/// at all.
148#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
149pub struct Redirect {
150 /// Where to fetch next.
151 pub target: u64,
152 /// Nullify the already-fetched delay slot.
153 ///
154 /// True only for a **branch-likely** form that was *not* taken. An ordinary
155 /// branch executes its delay slot whether or not it is taken; a likely branch
156 /// squashes it when not taken. Confusing the two silently runs or skips one
157 /// instruction per untaken branch.
158 pub nullify_delay_slot: bool,
159}
160
161/// A **coprocessor** access that `EX` resolved but must not itself perform.
162///
163/// Despite the name this now covers COP0, the TLB **and** COP1 control (`Cop1`).
164/// They share a variant because they share the stage split below, not because
165/// they are the same unit — the name is kept for churn reasons and this note
166/// exists so it does not mislead as COP1 grows in Sprint 3.
167///
168/// The stage split is the manual's, not a convenience: UM §4.6.9 describes the
169/// CP0 bypass interlock as firing when *"an instruction which caused an
170/// exception reaches the WB stage and the subsequent instruction in the DC stage
171/// requests a read of any CP0 register"* — so a coprocessor **read happens in
172/// DC** and a **write happens in WB**. Performing both in `EX` would make that
173/// interlock unexpressible, which is the same mistake ADR 0007 exists to prevent
174/// one level up.
175#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
176pub enum Cop0Access {
177 /// Read COP0 register `src` into GPR `dest`, in `DC`.
178 Read {
179 /// COP0 register number.
180 src: u8,
181 /// GPR destination.
182 dest: u8,
183 /// 64-bit (`DMFC0`) rather than 32-bit sign-extended (`MFC0`).
184 wide: bool,
185 },
186 /// A COP1 **control** access, performed where the state lives.
187 Cop1(Cop1Access),
188 /// A TLB instruction. Performed where the TLB lives, not in `EX`.
189 Tlb(TlbOp),
190 /// `ERET` — restore `PC` from `EPC`/`ErrorEPC`, clear `EXL`/`ERL`, clear
191 /// the link bit.
192 ///
193 /// Cannot be resolved in `EX` like an ordinary redirect, because the target
194 /// comes out of COP0 rather than out of the instruction.
195 Eret,
196 /// Write `value` to COP0 register `dest`, in `WB`.
197 Write {
198 /// COP0 register number.
199 dest: u8,
200 /// Value from `rt`.
201 value: u64,
202 /// 64-bit (`DMTC0`) rather than 32-bit (`MTC0`).
203 wide: bool,
204 },
205}
206
207/// The COP1 control moves (T-12-006). Arithmetic is Sprint 3.
208#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
209pub enum Cop1Access {
210 /// `MFC1`/`DMFC1` — move an FPR to a GPR.
211 ReadFpr {
212 /// FPR number (`fs`).
213 src: u8,
214 /// GPR destination.
215 dest: u8,
216 /// 64-bit (`DMFC1`) rather than 32-bit sign-extended (`MFC1`).
217 wide: bool,
218 },
219 /// `MTC1`/`DMTC1` — move a GPR to an FPR.
220 WriteFpr {
221 /// FPR number (`fs`).
222 dest: u8,
223 /// Value from `rt`.
224 value: u64,
225 /// 64-bit (`DMTC1`) rather than 32-bit (`MTC1`).
226 wide: bool,
227 },
228 /// A COP1 arithmetic operation, performed by the pipeline because it reads
229 /// two FPRs and writes a third, and `execute` has no FPR access.
230 Arith {
231 /// Format from `rs`: 16 = single, 17 = double.
232 fmt: u8,
233 /// Operation from `funct`.
234 funct: u8,
235 /// Second source FPR.
236 ft: u8,
237 /// First source FPR.
238 fs: u8,
239 /// Destination FPR.
240 fd: u8,
241 },
242 /// `CFC1` — read control register `src` into GPR `dest`.
243 ReadControl {
244 /// COP1 control register number.
245 src: u8,
246 /// GPR destination.
247 dest: u8,
248 },
249 /// `CTC1` — write `value` to control register `dest`.
250 WriteControl {
251 /// COP1 control register number.
252 dest: u8,
253 /// Value from `rt`.
254 value: u32,
255 },
256}
257
258/// The four TLB instructions.
259#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
260pub enum TlbOp {
261 /// `TLBR` — entry → COP0 registers.
262 Read,
263 /// `TLBWI` — COP0 registers → entry `Index`.
264 WriteIndexed,
265 /// `TLBWR` — COP0 registers → entry `Random`.
266 WriteRandom,
267 /// `TLBP` — probe, reporting through `Index`.
268 Probe,
269}
270
271/// The outcome of executing one instruction in `EX`.
272#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
273pub struct Executed {
274 /// What to commit at `WB`.
275 pub write_back: WriteBack,
276 /// Extra `PCycle`s the whole pipeline stalls for (multiply/divide only).
277 pub stall_cycles: u32,
278 /// A memory access for `DC` to perform, if any.
279 pub mem: Option<MemOp>,
280 /// A control-flow redirect, if this was a taken branch, a jump, or an
281 /// untaken branch-likely.
282 pub redirect: Option<Redirect>,
283 /// A COP0 access for `DC` (read) or `WB` (write) to perform.
284 pub cop0: Option<Cop0Access>,
285 /// The GPR a jump-and-link writes its return address to, if any.
286 ///
287 /// Carried separately from `write_back` because the **value** cannot be
288 /// computed here: the link is the address of the instruction that runs
289 /// after this one's delay slot, which is `pc + 8` only when the jump is
290 /// *not itself in a delay slot*. `EX` fills it in from the live `next_pc`,
291 /// which is that address by construction. See the jump/branch helper in this
292 /// module, and accuracy ledger C-19.
293 pub link: Option<u8>,
294}
295
296/// An `Executed` that does nothing: no write-back, no stall, no access, no
297/// redirect. Base for the arms that only set one field.
298const NOTHING: Executed = Executed {
299 write_back: WriteBack::None,
300 stall_cycles: 0,
301 mem: None,
302 redirect: None,
303 cop0: None,
304 link: None,
305};
306
307/// Sign-extend a 16-bit immediate — the arithmetic and `SLT` immediate forms.
308const fn sext_imm(imm: u16) -> u64 {
309 imm as i16 as i64 as u64
310}
311
312/// A jump or taken branch: redirect, and link if the form links.
313const fn control(d: Decoded, target: u64, nullify: bool) -> Executed {
314 Executed {
315 // The linking forms save the address *after* the delay slot, so a
316 // returning `JR $31` resumes past it rather than re-executing it.
317 //
318 // **That address is not always `pc + 8`.** When the jump is itself in
319 // another jump's delay slot, its own delay slot never runs — the outer
320 // jump has already redirected — so the instruction after it is the
321 // outer *target*, and the link is `target + 4`. n64-systemtest states
322 // it directly: "JAL in delay slot writes target address+4 of original
323 // jump into delay slot".
324 //
325 // So the register is named here and the value is filled in by `EX`
326 // from the live `next_pc`, which holds exactly that address in both
327 // cases. Computing `pc + 8` here was right for the ordinary case and
328 // silently wrong for the nested one.
329 write_back: WriteBack::None,
330 link: if d.dest == 0 { None } else { Some(d.dest) },
331 stall_cycles: 0,
332 mem: None,
333 redirect: Some(Redirect {
334 target,
335 nullify_delay_slot: nullify,
336 }),
337 cop0: None,
338 }
339}
340
341/// Resolve a conditional branch. Cannot fail — a branch raises no exception.
342const fn branch(d: Decoded, taken: bool, pc: u64) -> Executed {
343 if taken {
344 // Target is relative to the DELAY SLOT's address, not this one.
345 let target = pc
346 .wrapping_add(4)
347 .wrapping_add((sext_imm(d.imm) as i64 as u64) << 2);
348 return control(d, target, false);
349 }
350 // Not taken. A branch-likely nullifies its delay slot; an ordinary branch
351 // lets it run. Either way the linking forms STILL link -- BLTZAL writes $31
352 // even when the branch is not taken, which is easy to miss.
353 Executed {
354 // The link goes through `link`, not `write_back`, for the same reason
355 // the taken path does: a NOT-taken `BGEZAL` in another jump's delay
356 // slot still links, and still links to the OUTER target + 4. Computing
357 // `pc + 8` here was the one remaining place that formula survived.
358 link: if d.dest == 0 { None } else { Some(d.dest) },
359 write_back: WriteBack::None,
360 stall_cycles: 0,
361 mem: None,
362 cop0: None,
363 redirect: if d.op.is_likely() {
364 Some(Redirect {
365 target: pc.wrapping_add(8),
366 nullify_delay_slot: true,
367 })
368 } else {
369 None
370 },
371 }
372}
373
374/// A conditional trap.
375///
376/// # Errors
377/// [`Exception::Trap`] when the condition holds.
378const fn trap_if(cond: bool) -> Result<Executed, Exception> {
379 if cond {
380 Err(Exception::Trap)
381 } else {
382 Ok(Executed {
383 link: None,
384 write_back: WriteBack::None,
385 stall_cycles: 0,
386 mem: None,
387 redirect: None,
388 cop0: None,
389 })
390 }
391}
392
393/// Zero-extend a 16-bit immediate — the *logical* immediate forms only.
394///
395/// This asymmetry is real and easy to get wrong: `ADDI` sign-extends while
396/// `ANDI`/`ORI`/`XORI` zero-extend, so `ORI $t0, $0, 0xFFFF` yields `0x0000FFFF`
397/// and not `0xFFFFFFFFFFFFFFFF`.
398const fn zext_imm(imm: u16) -> u64 {
399 imm as u64
400}
401
402/// Execute one decoded instruction.
403///
404/// `rs_val` / `rt_val` are the register values resolved through the bypass
405/// network at `EX`; `hilo` is the current multiply-divide pair; `pc` is the
406/// address of *this* instruction, needed by the control-flow forms.
407///
408/// # Errors
409///
410/// [`Exception::Overflow`] from the trapping arithmetic forms, and
411/// [`Exception::ReservedInstruction`] for anything not yet decoded. Returning
412/// `Reserved` rather than treating an unknown encoding as a `NOP` is deliberate:
413/// a missing opcode should be loud, not silently produce wrong results.
414#[allow(clippy::too_many_lines)]
415// a flat match over the opcode table
416// `rs_val`/`rt_val` trip `similar_names`, but they mirror the MIPS operand
417// naming used throughout the manual and this crate. Any pair dissimilar enough
418// to satisfy the lint would be less clear, so the names stay and the lint goes.
419#[allow(clippy::similar_names)]
420/// `fp_condition` is `FCSR.C`, which only the `BC1` family reads. It is passed
421/// in rather than reached for because this function is pure and has no view of
422/// the coprocessor state — and passing it as a parameter makes every call site a
423/// compile error until it supplies one, which is what stopped `BC1` from being
424/// wired up to a stale or defaulted condition.
425pub const fn execute(
426 d: Decoded,
427 rs_val: u64,
428 rt_val: u64,
429 hilo: HiLo,
430 pc: u64,
431 fp_condition: bool,
432) -> Result<Executed, Exception> {
433 // Most instructions write one general register with no stall.
434 macro_rules! gpr {
435 ($v:expr) => {
436 Ok(Executed {
437 link: None,
438 write_back: WriteBack::Gpr {
439 dest: d.dest,
440 value: $v,
441 },
442 stall_cycles: 0,
443 mem: None,
444 redirect: None,
445 cop0: None,
446 })
447 };
448 }
449 // A load: EX resolves the address, DC performs the access and produces the
450 // write-back. Nothing is committed here.
451 macro_rules! mem_load {
452 ($kind:expr) => {
453 Ok(Executed {
454 link: None,
455 write_back: WriteBack::None,
456 stall_cycles: 0,
457 mem: Some(MemOp::Load {
458 kind: $kind,
459 addr: rs_val.wrapping_add(sext_imm(d.imm)),
460 dest: d.dest,
461 }),
462 redirect: None,
463 cop0: None,
464 })
465 };
466 }
467 macro_rules! mem_store {
468 ($kind:expr) => {
469 Ok(Executed {
470 link: None,
471 write_back: WriteBack::None,
472 stall_cycles: 0,
473 mem: Some(MemOp::Store {
474 kind: $kind,
475 addr: rs_val.wrapping_add(sext_imm(d.imm)),
476 value: rt_val,
477 }),
478 redirect: None,
479 cop0: None,
480 })
481 };
482 }
483 // Multiply/divide write HI/LO and stall the ENTIRE pipeline for the
484 // documented count (UM Table 3-12) -- they are not background operations.
485 macro_rules! muldiv {
486 ($res:expr, $kind:expr) => {
487 Ok(Executed {
488 link: None,
489 write_back: WriteBack::HiLo($res),
490 stall_cycles: alu::muldiv_stall_cycles($kind),
491 mem: None,
492 redirect: None,
493 cop0: None,
494 })
495 };
496 }
497
498 match d.op {
499 Op::Reserved => Err(Exception::ReservedInstruction),
500
501 // --- arithmetic, register form
502 Op::Add => match alu::add(rs_val, rt_val) {
503 Ok(v) => gpr!(v),
504 Err(e) => Err(e),
505 },
506 Op::Addu => gpr!(alu::addu(rs_val, rt_val)),
507 Op::Sub => match alu::sub(rs_val, rt_val) {
508 Ok(v) => gpr!(v),
509 Err(e) => Err(e),
510 },
511 Op::Subu => gpr!(alu::subu(rs_val, rt_val)),
512 Op::Dadd => match alu::dadd(rs_val, rt_val) {
513 Ok(v) => gpr!(v),
514 Err(e) => Err(e),
515 },
516 Op::Daddu => gpr!(alu::daddu(rs_val, rt_val)),
517 Op::Dsub => match alu::dsub(rs_val, rt_val) {
518 Ok(v) => gpr!(v),
519 Err(e) => Err(e),
520 },
521 Op::Dsubu => gpr!(alu::dsubu(rs_val, rt_val)),
522 Op::Slt => gpr!(alu::slt(rs_val, rt_val)),
523 Op::Sltu => gpr!(alu::sltu(rs_val, rt_val)),
524
525 // --- logical, register form
526 Op::And => gpr!(alu::and(rs_val, rt_val)),
527 Op::Or => gpr!(alu::or(rs_val, rt_val)),
528 Op::Xor => gpr!(alu::xor(rs_val, rt_val)),
529 Op::Nor => gpr!(alu::nor(rs_val, rt_val)),
530
531 // --- immediate forms. Note the sign/zero-extension asymmetry.
532 Op::Addi => match alu::add(rs_val, sext_imm(d.imm)) {
533 Ok(v) => gpr!(v),
534 Err(e) => Err(e),
535 },
536 Op::Addiu => gpr!(alu::addu(rs_val, sext_imm(d.imm))),
537 Op::Daddi => match alu::dadd(rs_val, sext_imm(d.imm)) {
538 Ok(v) => gpr!(v),
539 Err(e) => Err(e),
540 },
541 Op::Daddiu => gpr!(alu::daddu(rs_val, sext_imm(d.imm))),
542 Op::Slti => gpr!(alu::slt(rs_val, sext_imm(d.imm))),
543 Op::Sltiu => gpr!(alu::sltu(rs_val, sext_imm(d.imm))),
544 Op::Andi => gpr!(alu::and(rs_val, zext_imm(d.imm))),
545 Op::Ori => gpr!(alu::or(rs_val, zext_imm(d.imm))),
546 Op::Xori => gpr!(alu::xor(rs_val, zext_imm(d.imm))),
547 Op::Lui => gpr!(alu::lui(d.imm)),
548
549 // --- shifts. The immediate forms shift `rt`; the variable forms take
550 // the amount from `rs` (masked by the helper).
551 Op::Sll => gpr!(alu::sll(rt_val, d.sa)),
552 Op::Srl => gpr!(alu::srl(rt_val, d.sa)),
553 Op::Sra => gpr!(alu::sra(rt_val, d.sa)),
554 Op::Dsll | Op::Dsll32 => gpr!(alu::dsll(rt_val, d.sa)),
555 Op::Dsrl | Op::Dsrl32 => gpr!(alu::dsrl(rt_val, d.sa)),
556 Op::Dsra | Op::Dsra32 => gpr!(alu::dsra(rt_val, d.sa)),
557 Op::Sllv => gpr!(alu::sll(rt_val, rs_val as u32)),
558 Op::Srlv => gpr!(alu::srl(rt_val, rs_val as u32)),
559 Op::Srav => gpr!(alu::sra(rt_val, rs_val as u32)),
560 Op::Dsllv => gpr!(alu::dsll(rt_val, rs_val as u32)),
561 Op::Dsrlv => gpr!(alu::dsrl(rt_val, rs_val as u32)),
562 Op::Dsrav => gpr!(alu::dsra(rt_val, rs_val as u32)),
563
564 // --- multiply / divide
565 Op::Mult => muldiv!(alu::mult(rs_val, rt_val), MulDiv::Mult),
566 Op::Multu => muldiv!(alu::multu(rs_val, rt_val), MulDiv::Multu),
567 Op::Div => muldiv!(alu::div(rs_val, rt_val), MulDiv::Div),
568 Op::Divu => muldiv!(alu::divu(rs_val, rt_val), MulDiv::Divu),
569 Op::Dmult => muldiv!(alu::dmult(rs_val, rt_val), MulDiv::Dmult),
570 Op::Dmultu => muldiv!(alu::dmultu(rs_val, rt_val), MulDiv::Dmultu),
571 Op::Ddiv => muldiv!(alu::ddiv(rs_val, rt_val), MulDiv::Ddiv),
572 Op::Ddivu => muldiv!(alu::ddivu(rs_val, rt_val), MulDiv::Ddivu),
573
574 // --- HI/LO moves
575 Op::Mfhi => gpr!(hilo.hi),
576 Op::Mflo => gpr!(hilo.lo),
577 Op::Mthi => Ok(Executed {
578 link: None,
579 write_back: WriteBack::Hi(rs_val),
580 stall_cycles: 0,
581 mem: None,
582 redirect: None,
583 cop0: None,
584 }),
585 Op::Mtlo => Ok(Executed {
586 link: None,
587 write_back: WriteBack::Lo(rs_val),
588 stall_cycles: 0,
589 mem: None,
590 redirect: None,
591 cop0: None,
592 }),
593
594 // --- memory. EX resolves the effective address only; DC performs the
595 // access. The address is base + SIGN-extended offset, always.
596 Op::Lb => mem_load!(LoadKind::SignedByte),
597 Op::Lbu => mem_load!(LoadKind::UnsignedByte),
598 Op::Lh => mem_load!(LoadKind::SignedHalf),
599 Op::Lhu => mem_load!(LoadKind::UnsignedHalf),
600 Op::Lw => mem_load!(LoadKind::SignedWord),
601 Op::Lwu => mem_load!(LoadKind::UnsignedWord),
602 Op::Ld => mem_load!(LoadKind::Double),
603 Op::Sb => mem_store!(StoreKind::Byte),
604 Op::Sh => mem_store!(StoreKind::Half),
605 Op::Sw => mem_store!(StoreKind::Word),
606 Op::Sd => mem_store!(StoreKind::Double),
607 // The synchronization pair. `EX` treats them exactly like their ordinary
608 // counterparts; all the link-bit behavior is in `DC`, because that is
609 // where the bus access it is conditional on happens.
610 Op::Ll => Ok(Executed {
611 link: None,
612 write_back: WriteBack::None,
613 stall_cycles: 0,
614 mem: Some(MemOp::LinkedLoad {
615 kind: LoadKind::SignedWord,
616 addr: rs_val.wrapping_add(sext_imm(d.imm)),
617 dest: d.dest,
618 }),
619 redirect: None,
620 cop0: None,
621 }),
622 Op::Lld => Ok(Executed {
623 link: None,
624 write_back: WriteBack::None,
625 stall_cycles: 0,
626 mem: Some(MemOp::LinkedLoad {
627 kind: LoadKind::Double,
628 addr: rs_val.wrapping_add(sext_imm(d.imm)),
629 dest: d.dest,
630 }),
631 redirect: None,
632 cop0: None,
633 }),
634 Op::Sc => Ok(Executed {
635 link: None,
636 write_back: WriteBack::None,
637 stall_cycles: 0,
638 mem: Some(MemOp::ConditionalStore {
639 kind: StoreKind::Word,
640 addr: rs_val.wrapping_add(sext_imm(d.imm)),
641 value: rt_val,
642 dest: d.dest,
643 }),
644 redirect: None,
645 cop0: None,
646 }),
647 Op::Scd => Ok(Executed {
648 link: None,
649 write_back: WriteBack::None,
650 stall_cycles: 0,
651 mem: Some(MemOp::ConditionalStore {
652 kind: StoreKind::Double,
653 addr: rs_val.wrapping_add(sext_imm(d.imm)),
654 value: rt_val,
655 dest: d.dest,
656 }),
657 redirect: None,
658 cop0: None,
659 }),
660 // --- control flow. `pc` is this instruction's address; the delay slot
661 // is at `pc + 4` and the instruction after it at `pc + 8`, which is what
662 // the linking forms save.
663 Op::J | Op::Jal => {
664 // The 26-bit region form keeps the top 4 bits of the DELAY SLOT's
665 // address, not this instruction's -- they differ across a 256 MiB
666 // boundary, which is exactly where a naive implementation breaks.
667 let region = pc.wrapping_add(4) & 0xFFFF_FFFF_F000_0000;
668 let target = region | ((d.target as u64) << 2);
669 Ok(control(d, target, false))
670 }
671 // JR and JALR differ only in whether decode gave them a destination:
672 // `control` links iff `d.dest != 0`, so one arm serves both.
673 Op::Jr | Op::Jalr => Ok(control(d, rs_val, false)),
674
675 // `BC1` — the FP condition is the predicate; the target arithmetic and
676 // the branch-likely nullification are shared with every other branch.
677 Op::Bc1t | Op::Bc1tl => Ok(branch(d, fp_condition, pc)),
678 Op::Bc1f | Op::Bc1fl => Ok(branch(d, !fp_condition, pc)),
679
680 Op::Beq | Op::Beql => Ok(branch(d, rs_val == rt_val, pc)),
681 Op::Bne | Op::Bnel => Ok(branch(d, rs_val != rt_val, pc)),
682 Op::Blez | Op::Blezl => Ok(branch(d, (rs_val as i64) <= 0, pc)),
683 Op::Bgtz | Op::Bgtzl => Ok(branch(d, (rs_val as i64) > 0, pc)),
684 Op::Bltz | Op::Bltzl | Op::Bltzal | Op::Bltzall => Ok(branch(d, (rs_val as i64) < 0, pc)),
685 Op::Bgez | Op::Bgezl | Op::Bgezal | Op::Bgezall => Ok(branch(d, (rs_val as i64) >= 0, pc)),
686
687 // --- traps. The comparison is 64-bit; the `*U` forms are unsigned.
688 Op::Tge => trap_if((rs_val as i64) >= (rt_val as i64)),
689 Op::Tgeu => trap_if(rs_val >= rt_val),
690 Op::Tlt => trap_if((rs_val as i64) < (rt_val as i64)),
691 Op::Tltu => trap_if(rs_val < rt_val),
692 Op::Teq => trap_if(rs_val == rt_val),
693 Op::Tne => trap_if(rs_val != rt_val),
694 // The immediate trap forms SIGN-extend, including the unsigned
695 // comparisons -- the `U` refers to the comparison, not the extension.
696 Op::Tgei => trap_if((rs_val as i64) >= (sext_imm(d.imm) as i64)),
697 Op::Tgeiu => trap_if(rs_val >= sext_imm(d.imm)),
698 Op::Tlti => trap_if((rs_val as i64) < (sext_imm(d.imm) as i64)),
699 Op::Tltiu => trap_if(rs_val < sext_imm(d.imm)),
700 Op::Teqi => trap_if(rs_val == sext_imm(d.imm)),
701 Op::Tnei => trap_if(rs_val != sext_imm(d.imm)),
702
703 // "all load/store instructions in this processor are executed in program
704 // order since the SYNC instruction is handled as a NOP" (UM §3.1). It
705 // retires normally -- what it must NOT do is raise reserved-instruction.
706 // COP0 access. `rd` is the COP0 register; `EX` only resolves which.
707 Op::Mfc0 | Op::Dmfc0 => Ok(Executed {
708 link: None,
709 cop0: Some(Cop0Access::Read {
710 src: d.rd,
711 dest: d.dest,
712 wide: matches!(d.op, Op::Dmfc0),
713 }),
714 ..NOTHING
715 }),
716 // `fs` is the COP1 control register, encoded in the `rd` field.
717 Op::Mfc1 | Op::Dmfc1 => Ok(Executed {
718 link: None,
719 cop0: Some(Cop0Access::Cop1(Cop1Access::ReadFpr {
720 src: d.rd,
721 dest: d.dest,
722 wide: matches!(d.op, Op::Dmfc1),
723 })),
724 ..NOTHING
725 }),
726 Op::Mtc1 | Op::Dmtc1 => Ok(Executed {
727 link: None,
728 cop0: Some(Cop0Access::Cop1(Cop1Access::WriteFpr {
729 dest: d.rd,
730 value: rt_val,
731 wide: matches!(d.op, Op::Dmtc1),
732 })),
733 ..NOTHING
734 }),
735 // FP loads and stores resolve their address exactly like the integer
736 // forms; only the register file they land in differs.
737 Op::Lwc1 | Op::Ldc1 | Op::Swc1 | Op::Sdc1 => Ok(Executed {
738 link: None,
739 mem: Some(MemOp::Fp {
740 op: d.op,
741 addr: rs_val.wrapping_add(sext_imm(d.imm)),
742 ft: d.rt,
743 }),
744 ..NOTHING
745 }),
746 Op::Cfc1 => Ok(Executed {
747 link: None,
748 cop0: Some(Cop0Access::Cop1(Cop1Access::ReadControl {
749 src: d.rd,
750 dest: d.dest,
751 })),
752 ..NOTHING
753 }),
754 Op::Ctc1 => Ok(Executed {
755 link: None,
756 cop0: Some(Cop0Access::Cop1(Cop1Access::WriteControl {
757 dest: d.rd,
758 value: rt_val as u32,
759 })),
760 ..NOTHING
761 }),
762 // Two distinct cases that share one behavior -- retire with no
763 // architectural effect -- and are merged only because clippy rejects
764 // identical arms:
765 //
766 // - `Cop1Unimplemented`: a valid COP1 encoding we do not implement.
767 // Raising here would be wrong -- with `Status.CU1` SET hardware would
768 // execute it, so pretending otherwise would make Sprint 3's arrival a
769 // behavior change rather than an addition. The coprocessor-usable
770 // check happens in the pipeline.
771 // - `Cop0Extension`: a COP0 CO instruction in the emux `funct`
772 // 0x20-0x3F extension range, inert on hardware (ledger C-8). Notably
773 // the target GPR is **not** written, so a probe reads back whatever
774 // was already there and concludes emux is absent.
775 Op::FpArith => Ok(Executed {
776 link: None,
777 cop0: Some(Cop0Access::Cop1(Cop1Access::Arith {
778 fmt: d.rs,
779 funct: (d.imm & 0x3F) as u8,
780 ft: d.rt,
781 fs: d.rd,
782 fd: d.sa as u8,
783 })),
784 ..NOTHING
785 }),
786 // Everything here is completed or refused in `EX` rather than by
787 // `execute`: the reserved-control forms trap there, and the COP2 moves
788 // need the latch, which `EX` owns. They are listed individually rather
789 // than left to a catch-all so that adding a coprocessor op forces a
790 // decision at this site.
791 // EMUX needs the bus, so it becomes a `MemOp` handled in `DC`. The
792 // `funct` is recovered from `sa`, which decode left holding it.
793 Op::Cop0Extension => Ok(Executed {
794 link: None,
795 mem: Some(MemOp::Emux {
796 funct: d.sa as u8,
797 code: d.imm,
798 ptr: rs_val,
799 len: rt_val,
800 dest: d.dest,
801 }),
802 ..NOTHING
803 }),
804 Op::Cop1Unimplemented
805 | Op::Cop2
806 | Op::Cop1ReservedControl
807 | Op::Cop2ReservedControl
808 | Op::Mfc2
809 | Op::Dmfc2
810 | Op::Mtc2 => Ok(NOTHING),
811 Op::Tlbr => Ok(Executed {
812 link: None,
813 cop0: Some(Cop0Access::Tlb(TlbOp::Read)),
814 ..NOTHING
815 }),
816 Op::Tlbwi => Ok(Executed {
817 link: None,
818 cop0: Some(Cop0Access::Tlb(TlbOp::WriteIndexed)),
819 ..NOTHING
820 }),
821 Op::Tlbwr => Ok(Executed {
822 link: None,
823 cop0: Some(Cop0Access::Tlb(TlbOp::WriteRandom)),
824 ..NOTHING
825 }),
826 Op::Tlbp => Ok(Executed {
827 link: None,
828 cop0: Some(Cop0Access::Tlb(TlbOp::Probe)),
829 ..NOTHING
830 }),
831 Op::Eret => Ok(Executed {
832 link: None,
833 cop0: Some(Cop0Access::Eret),
834 ..NOTHING
835 }),
836 Op::Mtc0 | Op::Dmtc0 => Ok(Executed {
837 link: None,
838 cop0: Some(Cop0Access::Write {
839 dest: d.rd,
840 value: rt_val,
841 wide: matches!(d.op, Op::Dmtc0),
842 }),
843 ..NOTHING
844 }),
845 // CACHE resolves its effective address like any load/store -- so it can
846 // raise a TLB fault, and DC must perform the translation -- but performs
847 // no data transfer. Modeled as a zero-width probe: see `MemOp::Cache`.
848 Op::Cache => Ok(Executed {
849 link: None,
850 mem: Some(MemOp::Cache {
851 addr: rs_val.wrapping_add(sext_imm(d.imm)),
852 op: d.rt,
853 }),
854 ..NOTHING
855 }),
856 Op::Sync => Ok(Executed {
857 link: None,
858 write_back: WriteBack::None,
859 stall_cycles: 0,
860 mem: None,
861 redirect: None,
862 cop0: None,
863 }),
864 Op::Syscall => Err(Exception::Syscall),
865 Op::Break => Err(Exception::Breakpoint),
866
867 Op::Lwl | Op::Lwr | Op::Ldl | Op::Ldr | Op::Swl | Op::Swr | Op::Sdl | Op::Sdr => {
868 Ok(Executed {
869 link: None,
870 write_back: WriteBack::None,
871 stall_cycles: 0,
872 mem: Some(MemOp::Unaligned {
873 op: d.op,
874 addr: rs_val.wrapping_add(sext_imm(d.imm)),
875 rt: rt_val,
876 dest: d.dest,
877 }),
878 redirect: None,
879 cop0: None,
880 })
881 }
882 }
883}
884
885#[cfg(test)]
886mod tests {
887 use super::*;
888 use crate::decode::decode;
889
890 #[allow(clippy::similar_names)] // mirrors the MIPS operand naming
891 fn run(word: u32, rs_val: u64, rt_val: u64) -> Result<Executed, Exception> {
892 execute(
893 decode(word),
894 rs_val,
895 rt_val,
896 HiLo { hi: 0, lo: 0 },
897 0,
898 false,
899 )
900 }
901 const fn r(funct: u32, rs: u32, rt: u32, rd: u32, sa: u32) -> u32 {
902 (rs << 21) | (rt << 16) | (rd << 11) | (sa << 6) | funct
903 }
904 const fn i(opcode: u32, rs: u32, rt: u32, imm: u16) -> u32 {
905 (opcode << 26) | (rs << 21) | (rt << 16) | imm as u32
906 }
907
908 #[test]
909 fn register_form_arithmetic_reaches_the_alu() {
910 // ADDU $3, $1, $2 with $1 = 2, $2 = 3
911 let e = run(r(0o41, 1, 2, 3, 0), 2, 3).unwrap();
912 assert_eq!(e.write_back, WriteBack::Gpr { dest: 3, value: 5 });
913 assert_eq!(e.stall_cycles, 0);
914 }
915
916 /// The immediate extension asymmetry: arithmetic sign-extends, logical
917 /// zero-extends. Getting this backwards is a classic MIPS bug — `ORI` with
918 /// 0xFFFF would produce all-ones instead of 0xFFFF.
919 #[test]
920 fn immediates_sign_extend_for_arithmetic_and_zero_extend_for_logical() {
921 // ADDIU $2, $0, -1 => sign-extended to 0xFFFF_FFFF_FFFF_FFFF
922 let e = run(i(0o11, 0, 2, 0xFFFF), 0, 0).unwrap();
923 assert_eq!(
924 e.write_back,
925 WriteBack::Gpr {
926 dest: 2,
927 value: u64::MAX
928 }
929 );
930 // ORI $2, $0, 0xFFFF => ZERO-extended to 0x0000_0000_0000_FFFF
931 let e = run(i(0o15, 0, 2, 0xFFFF), 0, 0).unwrap();
932 assert_eq!(
933 e.write_back,
934 WriteBack::Gpr {
935 dest: 2,
936 value: 0xFFFF
937 }
938 );
939 // ANDI and XORI likewise.
940 let e = run(i(0o14, 1, 2, 0xFFFF), u64::MAX, 0).unwrap();
941 assert_eq!(
942 e.write_back,
943 WriteBack::Gpr {
944 dest: 2,
945 value: 0xFFFF
946 }
947 );
948 }
949
950 #[test]
951 fn trapping_arithmetic_returns_overflow_instead_of_a_value() {
952 // ADD $3, $1, $2 with 0x7FFF_FFFF + 1
953 assert_eq!(
954 run(r(0o40, 1, 2, 3, 0), 0x7FFF_FFFF, 1),
955 Err(Exception::Overflow)
956 );
957 // ADDU with the same inputs must NOT trap.
958 assert!(run(r(0o41, 1, 2, 3, 0), 0x7FFF_FFFF, 1).is_ok());
959 }
960
961 #[test]
962 fn multiply_and_divide_write_hi_lo_and_stall_the_documented_count() {
963 // MULT $1, $2 with 6 * 7
964 let e = run(r(0o30, 1, 2, 0, 0), 6, 7).unwrap();
965 match e.write_back {
966 WriteBack::HiLo(hl) => assert_eq!((hl.hi, hl.lo), (0, 42)),
967 other => panic!("MULT should write HI/LO, got {other:?}"),
968 }
969 assert_eq!(e.stall_cycles, 5, "UM Table 3-12");
970 // DDIV is the expensive one.
971 assert_eq!(run(r(0o36, 1, 2, 0, 0), 100, 7).unwrap().stall_cycles, 69);
972 // ...and an ordinary ALU op stalls not at all.
973 assert_eq!(run(r(0o41, 1, 2, 3, 0), 1, 1).unwrap().stall_cycles, 0);
974 }
975
976 #[test]
977 fn hi_lo_moves_read_and_write_the_pair() {
978 // MFHI $5 reads HI
979 let e = execute(
980 decode(r(0o20, 0, 0, 5, 0)),
981 0,
982 0,
983 HiLo {
984 hi: 0xDEAD,
985 lo: 0xBEEF,
986 },
987 0,
988 false,
989 )
990 .unwrap();
991 assert_eq!(
992 e.write_back,
993 WriteBack::Gpr {
994 dest: 5,
995 value: 0xDEAD
996 }
997 );
998 // MFLO $5 reads LO
999 let e = execute(
1000 decode(r(0o22, 0, 0, 5, 0)),
1001 0,
1002 0,
1003 HiLo {
1004 hi: 0xDEAD,
1005 lo: 0xBEEF,
1006 },
1007 0,
1008 false,
1009 )
1010 .unwrap();
1011 assert_eq!(
1012 e.write_back,
1013 WriteBack::Gpr {
1014 dest: 5,
1015 value: 0xBEEF
1016 }
1017 );
1018 // MTHI $1 writes HI from rs
1019 let e = execute(
1020 decode(r(0o21, 1, 0, 0, 0)),
1021 0x1234,
1022 0,
1023 HiLo { hi: 0, lo: 0 },
1024 0,
1025 false,
1026 )
1027 .unwrap();
1028 assert_eq!(e.write_back, WriteBack::Hi(0x1234));
1029 }
1030
1031 /// An unimplemented opcode must be **loud**. Treating it as a `NOP` would let
1032 /// a program run past instructions that did nothing, producing wrong results
1033 /// with no indication of why.
1034 /// **The `SRAV` instruction path shares the `SRA` erratum.**
1035 ///
1036 /// Tested through `execute` rather than by calling `alu::sra`, because the
1037 /// risk being guarded against is precisely that `Op::Srav` stops routing
1038 /// through the shared helper — gaining its own "corrected" implementation
1039 /// while `SRA` stays right. A test that calls the helper directly cannot see
1040 /// that happen, which is what an earlier version of this test did.
1041 ///
1042 /// Source: `n64brew_wiki/markdown/VR4300.md` § Known Bugs.
1043 #[test]
1044 fn the_srav_instruction_path_shares_the_sra_erratum() {
1045 let rt = 0x0123_4567_89AB_CDEF;
1046 // SRAV $3, $2, $1 -- amount from rs, value from rt.
1047 let word = r(0o07, 1, 2, 3, 0);
1048 for (amount, want) in [
1049 (1u64, 0xFFFF_FFFF_C4D5_E6F7u64),
1050 (8, 0x0000_0000_6789_ABCD),
1051 (16, 0x0000_0000_4567_89AB),
1052 (31, 0x0000_0000_0246_8ACF),
1053 ] {
1054 let e = run(word, amount, rt).unwrap();
1055 assert_eq!(
1056 e.write_back,
1057 WriteBack::Gpr {
1058 dest: 3,
1059 value: want
1060 },
1061 "SRAV by {amount} must leak the upper half, as SRA does"
1062 );
1063 }
1064 // And the immediate form agrees with it, through its own path.
1065 let e = run(r(0o03, 0, 2, 3, 16), 0, rt).unwrap();
1066 assert_eq!(
1067 e.write_back,
1068 WriteBack::Gpr {
1069 dest: 3,
1070 value: 0x0000_0000_4567_89AB
1071 },
1072 "SRA and SRAV must not diverge"
1073 );
1074 }
1075
1076 #[test]
1077 fn unimplemented_opcodes_raise_reserved_instruction() {
1078 // These must be encodings the VR4300 genuinely leaves UNASSIGNED, not
1079 // merely ones this project has not implemented yet. Primary opcodes
1080 // 0o34..0o37 are reserved on MIPS III, and SPECIAL funct 0o01 is unused.
1081 // Earlier revisions of this test used LW and then BEQ, and had to be
1082 // repointed each time that opcode landed -- which made the test track
1083 // implementation progress instead of the architecture.
1084 assert_eq!(
1085 run(i(0o35, 1, 2, 0), 0, 0),
1086 Err(Exception::ReservedInstruction)
1087 );
1088 assert_eq!(
1089 run(r(0o01, 1, 2, 3, 0), 0, 0),
1090 Err(Exception::ReservedInstruction)
1091 );
1092 }
1093
1094 /// `SLL $0, $0, 0` is `NOP`: it executes successfully and its write-back
1095 /// targets `$zero`, which `Regs::write` discards.
1096 #[test]
1097 fn nop_executes_and_commits_nothing() {
1098 let e = run(0, 0, 0).unwrap();
1099 assert_eq!(e.write_back, WriteBack::Gpr { dest: 0, value: 0 });
1100 assert_eq!(e.stall_cycles, 0);
1101 }
1102}