javm_exec/regs.rs
1//! Register state: 15 general-purpose 64-bit registers + PC.
2//!
3//! PVM2 is RV64E — a 16-register integer base (`x0`–`x15`) — so it has the
4//! full **15** GPRs (`x1`, `x2`, `x5`–`x15`, plus `x3`, `x4`) with `x0`
5//! hardwired to zero, plus an instruction pointer. `x3`/`x4` map to the two
6//! *high* slots (13, 14) so the 13 commonly-used registers keep slots
7//! `0..=12`; the recompiler holds those 13 in host registers and **spills**
8//! `x3`/`x4` to memory (see [`reg_is_spilled`]). Only `x16`–`x31` (which do
9//! not exist in the E base) remain reserved.
10
11/// Number of general-purpose registers.
12pub const REG_COUNT: usize = 15;
13
14/// Classification of a 5-bit RV register index in PVM2.
15///
16/// PVM2 is an RV64E base — a 16-register file (`x0`–`x15`). This is the
17/// **single source of truth** for every place that needs register
18/// classification: the gas-simulator slot map, the recompiler's codegen
19/// slot map, the interpreter's register file access, the spilled-register
20/// routing, and the reserved-register check both engines use. They all
21/// derive from [`reg_class`] rather than re-encoding the valid/reserved
22/// sets (which is how `rv_is_reserved` once drifted to miss `x16..x31`).
23#[derive(Clone, Copy, PartialEq, Eq, Debug)]
24pub enum RegClass {
25 /// `x0` — hardwired zero. Valid, but has no GPR slot.
26 Zero,
27 /// `x1`, `x2`, `x5`–`x15`, `x3`, `x4` — general-purpose; the payload is
28 /// the slot `0..=14` into [`Regs::gpr`]. Slots `0..=12` are the 13
29 /// commonly-used registers (`x1`, `x2`, `x5`–`x15`); slots `13`/`14` are
30 /// `x3`/`x4`, which the recompiler spills to memory ([`reg_is_spilled`]).
31 Gpr(u8),
32 /// `x16`–`x31` — do not exist in RV64E (a 16-register base), so naming
33 /// one is an illegal encoding. Such an instruction is a reserved
34 /// encoding and panics if executed.
35 Reserved,
36}
37
38/// Classify a 5-bit RV register index (low 5 bits of `x`). See [`RegClass`].
39#[inline]
40pub const fn reg_class(x: u8) -> RegClass {
41 match x & 31 {
42 0 => RegClass::Zero,
43 1 => RegClass::Gpr(0),
44 2 => RegClass::Gpr(1),
45 3 => RegClass::Gpr(13), // x3 → high spill slot
46 4 => RegClass::Gpr(14), // x4 → high spill slot
47 n @ 5..=15 => RegClass::Gpr(n - 3),
48 _ => RegClass::Reserved, // x16..x31 only
49 }
50}
51
52/// PVM2 GPR slot (`0..=14`) for `x`, or `0xFF` if `x` has no slot — `x0`
53/// (hardwired zero) *or* a reserved register (`x16..x31`). The gas
54/// simulator reads `0xFF` as "no dependency / no write"; both engines' gas
55/// paths use this (the recompiler via the const-folded [`REG_SLOT_LUT`]) so
56/// gas agrees bit-for-bit. Note `0xFF` conflates `x0` with reserved — use
57/// [`reg_is_reserved`] when that distinction matters.
58#[inline]
59pub const fn reg_slot_or_ff(x: u8) -> u8 {
60 match reg_class(x) {
61 RegClass::Gpr(s) => s,
62 _ => 0xFF,
63 }
64}
65
66/// True iff `x` is a *reserved* register (`x16..x31` — they do not exist in
67/// the RV64E base) — as opposed to `x0`, which also lacks a slot but is
68/// valid. Drives the reserved-encoding (illegal) check in both engines.
69#[inline]
70pub const fn reg_is_reserved(x: u8) -> bool {
71 matches!(reg_class(x), RegClass::Reserved)
72}
73
74/// True iff `x` is a *spilled* register — `x3` or `x4`, the two GPRs that
75/// map to the high slots (13, 14). They are real, fully-valid registers
76/// (the interpreter executes them as ordinary GPRs), but the x86-64
77/// recompiler's host register file is exhausted by the other 13 slots, so
78/// it holds `x3`/`x4` in memory and materialises them per access. The
79/// recompiler uses this to route an `x3`/`x4` instruction to its cold spill
80/// path; the gas model uses it to charge the memory-spill cost.
81#[inline]
82pub const fn reg_is_spilled(x: u8) -> bool {
83 matches!(reg_class(x), RegClass::Gpr(13) | RegClass::Gpr(14))
84}
85
86/// 32-entry const-folded copy of [`reg_slot_or_ff`] for the recompiler's
87/// codegen/gas hot path — a single load beats the range-match (the
88/// profiler showed the match at ~8.8% of compile). Generated from
89/// [`reg_class`], so it cannot drift from the canonical classification.
90pub const REG_SLOT_LUT: [u8; 32] = {
91 let mut t = [0u8; 32];
92 let mut x = 0u8;
93 while x < 32 {
94 t[x as usize] = reg_slot_or_ff(x);
95 x += 1;
96 }
97 t
98};
99
100/// Full register state: 15 GPRs + PC.
101#[derive(Clone, Debug, PartialEq, Eq)]
102pub struct Regs {
103 /// General-purpose registers, including the two spilled x3/x4 slots.
104 pub gpr: [u64; REG_COUNT],
105 /// Program counter — a code byte-offset, not a memory address.
106 /// (Register-held code addresses, e.g. a saved return address or an
107 /// `auipc` result, are guest VAs `code_base + offset`.)
108 pub pc: u64,
109}
110
111impl Regs {
112 /// All zeros, PC = 0.
113 pub fn new() -> Self {
114 Self::default()
115 }
116
117 /// Read register `i`. Returns 0 if `i >= REG_COUNT` (defensive;
118 /// callers should validate the opcode first).
119 pub fn read(&self, i: usize) -> u64 {
120 self.gpr.get(i).copied().unwrap_or(0)
121 }
122
123 /// Write register `i`. No-op if `i >= REG_COUNT`.
124 pub fn write(&mut self, i: usize, v: u64) {
125 if let Some(slot) = self.gpr.get_mut(i) {
126 *slot = v;
127 }
128 }
129}
130
131impl Default for Regs {
132 fn default() -> Self {
133 Self {
134 gpr: [0u64; REG_COUNT],
135 pc: 0,
136 }
137 }
138}