Skip to main content

javm_fuzz/
encode.rs

1//! RV64E-subset instruction encoders — the single source the generator and the
2//! decode round-trip test both drive off.
3//!
4//! The two existing x3/x4 test files (`javm-recompiler-x86/tests/x3_x4_spill.rs`
5//! and `javm-bench/tests/x3_x4_differential.rs`) each hand-rolled a handful of
6//! ad-hoc encoders. This centralizes them and extends to the full implemented
7//! ISA via the [`OPS`] spec table, validated against `javm_exec::decode` in the
8//! tests below (every op must round-trip to a non-`Reserved` instruction).
9
10/// Pack instruction words into a little-endian byte stream.
11pub fn enc(words: &[u32]) -> Vec<u8> {
12    let mut v = Vec::with_capacity(words.len() * 4);
13    for w in words {
14        v.extend_from_slice(&w.to_le_bytes());
15    }
16    v
17}
18
19// ---- Format encoders (private; named helpers + OPS table build on these) ----
20
21#[inline]
22fn r(opcode: u32, funct7: u32, funct3: u32, rd: u8, rs1: u8, rs2: u8) -> u32 {
23    (funct7 << 25)
24        | ((rs2 as u32) << 20)
25        | ((rs1 as u32) << 15)
26        | (funct3 << 12)
27        | ((rd as u32) << 7)
28        | opcode
29}
30
31#[inline]
32fn i(opcode: u32, funct3: u32, rd: u8, rs1: u8, imm: i32) -> u32 {
33    ((imm as u32 & 0xFFF) << 20)
34        | ((rs1 as u32) << 15)
35        | (funct3 << 12)
36        | ((rd as u32) << 7)
37        | opcode
38}
39
40#[inline]
41fn s(opcode: u32, funct3: u32, rs1: u8, rs2: u8, imm: i32) -> u32 {
42    let u = imm as u32;
43    (((u >> 5) & 0x7F) << 25)
44        | ((rs2 as u32) << 20)
45        | ((rs1 as u32) << 15)
46        | (funct3 << 12)
47        | ((u & 0x1F) << 7)
48        | opcode
49}
50
51#[inline]
52fn b_(opcode: u32, funct3: u32, rs1: u8, rs2: u8, imm: i32) -> u32 {
53    let u = imm as u32;
54    (((u >> 12) & 1) << 31)
55        | (((u >> 5) & 0x3F) << 25)
56        | ((rs2 as u32) << 20)
57        | ((rs1 as u32) << 15)
58        | (funct3 << 12)
59        | (((u >> 1) & 0xF) << 8)
60        | (((u >> 11) & 1) << 7)
61        | opcode
62}
63
64#[inline]
65fn u_(opcode: u32, rd: u8, imm20: u32) -> u32 {
66    ((imm20 & 0xFFFFF) << 12) | ((rd as u32) << 7) | opcode
67}
68
69/// 64-bit shift / Zbs-imm: `funct6` occupies imm[11:6], shamt imm[5:0].
70#[inline]
71fn i_shift64(opcode: u32, funct3: u32, funct6: u32, rd: u8, rs1: u8, shamt: u8) -> u32 {
72    let imm = ((funct6 & 0x3F) << 6) | (shamt as u32 & 0x3F);
73    i(opcode, funct3, rd, rs1, imm as i32)
74}
75
76/// 32-bit (W) shift: `funct7` occupies imm[11:5], shamt imm[4:0].
77#[inline]
78fn i_shift32(opcode: u32, funct3: u32, funct7: u32, rd: u8, rs1: u8, shamt: u8) -> u32 {
79    let imm = ((funct7 & 0x7F) << 5) | (shamt as u32 & 0x1F);
80    i(opcode, funct3, rd, rs1, imm as i32)
81}
82
83// ---- Major opcodes (7-bit, low two bits always 11 for non-compressed) ----
84const OP: u32 = 0x33; // OP        (R-type, 64)
85const OP_IMM: u32 = 0x13; // OP-IMM     (I-type, 64)
86const OP_IMM_32: u32 = 0x1B; // OP-IMM-32  (I-type, 32)
87const OP_32: u32 = 0x3B; // OP-32      (R-type, 32)
88const LOAD: u32 = 0x03;
89const STORE: u32 = 0x23;
90const BRANCH: u32 = 0x63;
91const LUI: u32 = 0x37;
92const AUIPC: u32 = 0x17;
93
94/// Instruction format — selects how [`encode_op`] places operands.
95#[derive(Clone, Copy, Debug, PartialEq, Eq)]
96pub enum Fmt {
97    /// `rd, rs1, rs2` (OP / OP-32). `aux` = funct7.
98    R,
99    /// `rd, rs1, imm12` (OP-IMM / OP-IMM-32 / LOAD).
100    I,
101    /// `rd, rs1, shamt6` (64-bit shift / Zbs-imm). `aux` = funct6.
102    IShift,
103    /// `rd, rs1, shamt5` (W shift). `aux` = funct7.
104    IShift32,
105    /// `rs1(base), rs2(val), imm12` (STORE).
106    Store,
107    /// `rs1, rs2, imm13` (BRANCH).
108    Branch,
109    /// `rd, imm20` (LUI / AUIPC).
110    U,
111    /// `rd, rs1` with a fixed function code (Zbb unary). `aux` = funct12.
112    Unary,
113}
114
115/// One implemented instruction: enough to encode it from operands.
116#[derive(Clone, Copy, Debug)]
117pub struct OpSpec {
118    pub name: &'static str,
119    pub fmt: Fmt,
120    pub opcode: u32,
121    pub funct3: u32,
122    /// funct7 (R / IShift32), funct6 (IShift), or funct12 (Unary).
123    pub aux: u32,
124}
125
126const fn op(name: &'static str, fmt: Fmt, opcode: u32, funct3: u32, aux: u32) -> OpSpec {
127    OpSpec {
128        name,
129        fmt,
130        opcode,
131        funct3,
132        aux,
133    }
134}
135
136impl OpSpec {
137    /// True for loads, stores, and branches — ops the (v1) register-only
138    /// generator skips because they need a backing memory window or
139    /// non-straight-line control flow. The rest (R/I/shift/unary/lui/auipc)
140    /// are pure register→register and total by construction.
141    pub fn touches_memory_or_control(&self) -> bool {
142        self.opcode == LOAD || matches!(self.fmt, Fmt::Store | Fmt::Branch)
143    }
144}
145
146/// Every instruction family the generator can emit. Validated against the
147/// decoder in `round_trip_all_ops` (each must decode to a non-`Reserved`
148/// instruction). Excludes terminators (`ecalli`/`trap`/`fallthrough`),
149/// `fence`, and anything reserved (SYSTEM, x3/x4, x16–31).
150#[rustfmt::skip]
151pub const OPS: &[OpSpec] = &[
152    // -- R-type, 64-bit (OP) --
153    op("add",  Fmt::R, OP, 0, 0x00), op("sub", Fmt::R, OP, 0, 0x20),
154    op("sll",  Fmt::R, OP, 1, 0x00), op("slt", Fmt::R, OP, 2, 0x00),
155    op("sltu", Fmt::R, OP, 3, 0x00), op("xor", Fmt::R, OP, 4, 0x00),
156    op("srl",  Fmt::R, OP, 5, 0x00), op("sra", Fmt::R, OP, 5, 0x20),
157    op("or",   Fmt::R, OP, 6, 0x00), op("and", Fmt::R, OP, 7, 0x00),
158    // M
159    op("mul",  Fmt::R, OP, 0, 0x01), op("mulh",   Fmt::R, OP, 1, 0x01),
160    op("mulhsu", Fmt::R, OP, 2, 0x01), op("mulhu", Fmt::R, OP, 3, 0x01),
161    op("div",  Fmt::R, OP, 4, 0x01), op("divu",   Fmt::R, OP, 5, 0x01),
162    op("rem",  Fmt::R, OP, 6, 0x01), op("remu",   Fmt::R, OP, 7, 0x01),
163    // Zbb binary
164    op("min",  Fmt::R, OP, 4, 0x05), op("minu", Fmt::R, OP, 5, 0x05),
165    op("max",  Fmt::R, OP, 6, 0x05), op("maxu", Fmt::R, OP, 7, 0x05),
166    op("andn", Fmt::R, OP, 7, 0x20), op("orn",  Fmt::R, OP, 6, 0x20),
167    op("xnor", Fmt::R, OP, 4, 0x20), op("rol",  Fmt::R, OP, 1, 0x30),
168    op("ror",  Fmt::R, OP, 5, 0x30),
169    // Zba
170    op("sh1add", Fmt::R, OP, 2, 0x10), op("sh2add", Fmt::R, OP, 4, 0x10),
171    op("sh3add", Fmt::R, OP, 6, 0x10),
172    // Zbs
173    op("bclr", Fmt::R, OP, 1, 0x24), op("bext", Fmt::R, OP, 5, 0x24),
174    op("binv", Fmt::R, OP, 1, 0x34), op("bset", Fmt::R, OP, 1, 0x14),
175    // Zicond
176    op("czero.eqz", Fmt::R, OP, 5, 0x07), op("czero.nez", Fmt::R, OP, 7, 0x07),
177
178    // -- R-type, 32-bit (OP-32) --
179    op("addw", Fmt::R, OP_32, 0, 0x00), op("subw", Fmt::R, OP_32, 0, 0x20),
180    op("sllw", Fmt::R, OP_32, 1, 0x00), op("srlw", Fmt::R, OP_32, 5, 0x00),
181    op("sraw", Fmt::R, OP_32, 5, 0x20),
182    op("mulw", Fmt::R, OP_32, 0, 0x01), op("divw", Fmt::R, OP_32, 4, 0x01),
183    op("divuw", Fmt::R, OP_32, 5, 0x01), op("remw", Fmt::R, OP_32, 6, 0x01),
184    op("remuw", Fmt::R, OP_32, 7, 0x01),
185    op("adduw", Fmt::R, OP_32, 0, 0x04),
186    op("sh1add.uw", Fmt::R, OP_32, 2, 0x10), op("sh2add.uw", Fmt::R, OP_32, 4, 0x10),
187    op("sh3add.uw", Fmt::R, OP_32, 6, 0x10),
188    op("rolw", Fmt::R, OP_32, 1, 0x30), op("rorw", Fmt::R, OP_32, 5, 0x30),
189
190    // -- I-type ALU (OP-IMM) --
191    op("addi", Fmt::I, OP_IMM, 0, 0), op("slti", Fmt::I, OP_IMM, 2, 0),
192    op("sltiu", Fmt::I, OP_IMM, 3, 0), op("xori", Fmt::I, OP_IMM, 4, 0),
193    op("ori", Fmt::I, OP_IMM, 6, 0), op("andi", Fmt::I, OP_IMM, 7, 0),
194    op("addiw", Fmt::I, OP_IMM_32, 0, 0),
195
196    // -- I-type shift, 64-bit (OP-IMM, funct6) --
197    op("slli", Fmt::IShift, OP_IMM, 1, 0x00), op("srli", Fmt::IShift, OP_IMM, 5, 0x00),
198    op("srai", Fmt::IShift, OP_IMM, 5, 0x10), op("rori", Fmt::IShift, OP_IMM, 5, 0x18),
199    op("bclri", Fmt::IShift, OP_IMM, 1, 0x12), op("bexti", Fmt::IShift, OP_IMM, 5, 0x12),
200    op("bseti", Fmt::IShift, OP_IMM, 1, 0x0A), op("binvi", Fmt::IShift, OP_IMM, 1, 0x1A),
201    // NB: `slli.uw` is intentionally omitted — its 6-bit shamt overlaps the
202    // decoder's 7-bit funct7 check, so shamt ≥ 32 decodes as Reserved. The
203    // generator avoids it to keep every emitted instruction a live encoding.
204
205    // -- I-type shift, 32-bit (OP-IMM-32, funct7) --
206    op("slliw", Fmt::IShift32, OP_IMM_32, 1, 0x00), op("srliw", Fmt::IShift32, OP_IMM_32, 5, 0x00),
207    op("sraiw", Fmt::IShift32, OP_IMM_32, 5, 0x20), op("roriw", Fmt::IShift32, OP_IMM_32, 5, 0x30),
208
209    // -- Zbb unary (fixed funct12) --
210    op("clz", Fmt::Unary, OP_IMM, 1, 0x600), op("ctz", Fmt::Unary, OP_IMM, 1, 0x601),
211    op("cpop", Fmt::Unary, OP_IMM, 1, 0x602), op("sext.b", Fmt::Unary, OP_IMM, 1, 0x604),
212    op("sext.h", Fmt::Unary, OP_IMM, 1, 0x605), op("orc.b", Fmt::Unary, OP_IMM, 5, 0x287),
213    op("rev8", Fmt::Unary, OP_IMM, 5, 0x6B8),
214    op("clzw", Fmt::Unary, OP_IMM_32, 1, 0x600), op("ctzw", Fmt::Unary, OP_IMM_32, 1, 0x601),
215    op("cpopw", Fmt::Unary, OP_IMM_32, 1, 0x602),
216    // NB: `zext.h` is intentionally omitted — this decoder recognizes it via the
217    // RV32-style OP (0x33) encoding rather than the standard RV64 OP-32, so the
218    // generator avoids it to keep the op table on uncontested encodings.
219
220    // -- Loads / Stores --
221    op("lb", Fmt::I, LOAD, 0, 0), op("lh", Fmt::I, LOAD, 1, 0),
222    op("lw", Fmt::I, LOAD, 2, 0), op("ld", Fmt::I, LOAD, 3, 0),
223    op("lbu", Fmt::I, LOAD, 4, 0), op("lhu", Fmt::I, LOAD, 5, 0),
224    op("lwu", Fmt::I, LOAD, 6, 0),
225    op("sb", Fmt::Store, STORE, 0, 0), op("sh", Fmt::Store, STORE, 1, 0),
226    op("sw", Fmt::Store, STORE, 2, 0), op("sd", Fmt::Store, STORE, 3, 0),
227
228    // -- Upper immediate --
229    op("lui", Fmt::U, LUI, 0, 0), op("auipc", Fmt::U, AUIPC, 0, 0),
230
231    // -- Branches --
232    op("beq", Fmt::Branch, BRANCH, 0, 0), op("bne", Fmt::Branch, BRANCH, 1, 0),
233    op("blt", Fmt::Branch, BRANCH, 4, 0), op("bge", Fmt::Branch, BRANCH, 5, 0),
234    op("bltu", Fmt::Branch, BRANCH, 6, 0), op("bgeu", Fmt::Branch, BRANCH, 7, 0),
235];
236
237/// Encode `op` with the given operands. Operands not used by the format are
238/// ignored (e.g. `rs2` for `Fmt::I`, `imm` for `Fmt::Unary`). For shift
239/// formats the low bits of `imm` are the shift amount; for `Fmt::U`, `imm` is
240/// the 20-bit upper immediate.
241pub fn encode_op(spec: &OpSpec, rd: u8, rs1: u8, rs2: u8, imm: i32) -> u32 {
242    match spec.fmt {
243        Fmt::R => r(spec.opcode, spec.aux, spec.funct3, rd, rs1, rs2),
244        Fmt::I => i(spec.opcode, spec.funct3, rd, rs1, imm),
245        Fmt::IShift => i_shift64(
246            spec.opcode,
247            spec.funct3,
248            spec.aux,
249            rd,
250            rs1,
251            (imm as u32 & 0x3F) as u8,
252        ),
253        Fmt::IShift32 => i_shift32(
254            spec.opcode,
255            spec.funct3,
256            spec.aux,
257            rd,
258            rs1,
259            (imm as u32 & 0x1F) as u8,
260        ),
261        Fmt::Store => s(spec.opcode, spec.funct3, rs1, rs2, imm),
262        Fmt::Branch => b_(spec.opcode, spec.funct3, rs1, rs2, imm),
263        Fmt::U => u_(spec.opcode, rd, imm as u32),
264        Fmt::Unary => i(spec.opcode, spec.funct3, rd, rs1, spec.aux as i32),
265    }
266}
267
268// ---- Named helpers (the fold, constant materialization, tests) ----
269
270/// `ecalli 0` — HostCall(0), the clean trampoline halt both engines surface as
271/// `exit_reason = 4`. Appended by the replay harness, not stored in vectors.
272pub const HALT: u32 = 0x0000_200B;
273
274pub fn addi(rd: u8, rs1: u8, imm: i32) -> u32 {
275    i(OP_IMM, 0, rd, rs1, imm)
276}
277pub fn add(rd: u8, rs1: u8, rs2: u8) -> u32 {
278    r(OP, 0x00, 0, rd, rs1, rs2)
279}
280pub fn sub(rd: u8, rs1: u8, rs2: u8) -> u32 {
281    r(OP, 0x20, 0, rd, rs1, rs2)
282}
283pub fn xor(rd: u8, rs1: u8, rs2: u8) -> u32 {
284    r(OP, 0x00, 4, rd, rs1, rs2)
285}
286pub fn div(rd: u8, rs1: u8, rs2: u8) -> u32 {
287    r(OP, 0x01, 4, rd, rs1, rs2)
288}
289pub fn rem(rd: u8, rs1: u8, rs2: u8) -> u32 {
290    r(OP, 0x01, 6, rd, rs1, rs2)
291}
292pub fn mulhsu(rd: u8, rs1: u8, rs2: u8) -> u32 {
293    r(OP, 0x01, 2, rd, rs1, rs2)
294}
295pub fn slli(rd: u8, rs1: u8, shamt: u8) -> u32 {
296    i_shift64(OP_IMM, 1, 0x00, rd, rs1, shamt)
297}
298pub fn rori(rd: u8, rs1: u8, shamt: u8) -> u32 {
299    i_shift64(OP_IMM, 5, 0x18, rd, rs1, shamt)
300}
301pub fn ld(rd: u8, rs1: u8, imm: i32) -> u32 {
302    i(LOAD, 3, rd, rs1, imm)
303}
304pub fn sd(rs1: u8, rs2: u8, imm: i32) -> u32 {
305    s(STORE, 3, rs1, rs2, imm)
306}
307pub fn lui(rd: u8, imm20: u32) -> u32 {
308    u_(LUI, rd, imm20)
309}
310pub fn beq(rs1: u8, rs2: u8, imm: i32) -> u32 {
311    b_(BRANCH, 0, rs1, rs2, imm)
312}
313
314/// Materialize an arbitrary `value` into `rd` using only `rd` (no scratch
315/// register): build MSB-first in 11-bit chunks via `addi`/`slli`. Each `addi`
316/// adds an 11-bit (always non-negative) chunk into freshly-zeroed low bits.
317pub fn li64(rd: u8, value: u64) -> Vec<u32> {
318    let chunk = |sh: u32| ((value >> sh) & 0x7FF) as i32;
319    let mut out = vec![addi(rd, 0, ((value >> 55) & 0x1FF) as i32)];
320    for sh in [44u32, 33, 22, 11, 0] {
321        out.push(slli(rd, rd, 11));
322        out.push(addi(rd, rd, chunk(sh)));
323    }
324    out
325}
326
327// ---- Signature epilogue (lossless state readout — see lib docs) ----
328
329/// Number of host-mapped register slots captured by the signature (slots
330/// 0..=12 → x1, x2, x5, x6, x7, x8–x15; see [`crate::oracle::slot_to_xreg`]).
331pub const SIG_REGS: usize = 13;
332
333/// Byte length of the register signature: one little-endian `u64` per captured
334/// slot. Fits in a single page and in `SCRATCHPAD_HEAD_LEN` (128).
335pub const SIG_BYTES: usize = SIG_REGS * 8;
336
337/// The x-register stored at signature slot `i` (the inverse of
338/// `javm_exec::regs::reg_slot_or_ff`, matching [`crate::oracle::slot_to_xreg`]).
339/// Slot 7 = x10 (the former fold `return_value`). The epilogue stores each at
340/// byte offset `8*i` of the signature region.
341pub const SIG_XREGS: [u8; SIG_REGS] = [1, 2, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
342
343/// Scratch base register for the signature stores. x3 is spilled — it is not in
344/// the captured set (slots 0..=12) and is invocation-local (dropped at exit), so
345/// clobbering it is invisible to the differential, and both engines agree on
346/// x3/x4 spill semantics (the `x3_x4_differential` net). Using it as the store
347/// base leaves every captured register untouched, so the stored values are the
348/// program's exact post-body register file.
349pub const SIG_BASE_REG: u8 = 3;
350
351/// Emit the signature epilogue (no terminator): materialize `sig_base` into the
352/// scratch base register, then `sd` each captured register to `sig_base + 8*i`.
353/// `sig_base` is the guest VA the scratchpad (`slot[0]`) DataCap maps at; the
354/// guest's stores CoW the region's pages, and the host reads the effective
355/// bytes back as the run's lossless register signature (vs the old lossy x10
356/// fold).
357pub fn signature_epilogue(sig_base: u32) -> Vec<u32> {
358    let mut out = li64(SIG_BASE_REG, sig_base as u64);
359    for (i, &xr) in SIG_XREGS.iter().enumerate() {
360        out.push(sd(SIG_BASE_REG, xr, (i * 8) as i32));
361    }
362    out
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368    use javm_exec::instruction::{Inst, decode};
369
370    fn decode1(w: u32) -> (Inst, u8) {
371        decode(&w.to_le_bytes()).unwrap_or_else(|| panic!("decode failed for {w:#010x}"))
372    }
373
374    #[test]
375    fn round_trip_all_ops() {
376        // Every OPS entry must decode to a recognized (non-Reserved) 4-byte
377        // instruction with valid registers. Collect all failures so a bad
378        // funct value names itself instead of failing on the first op.
379        let mut bad = Vec::new();
380        for spec in OPS {
381            // rd=x10, rs1=x11, rs2=x12; imm=4 (even, in range for every fmt).
382            let w = encode_op(spec, 10, 11, 12, 4);
383            match decode(&w.to_le_bytes()) {
384                Some((Inst::Reserved { .. }, _)) | None => {
385                    bad.push(format!("{} -> Reserved/None ({w:#010x})", spec.name))
386                }
387                Some((_, 4)) => {}
388                Some((_, len)) => bad.push(format!("{} -> len {len} ({w:#010x})", spec.name)),
389            }
390        }
391        assert!(
392            bad.is_empty(),
393            "encoder/decoder mismatch:\n  {}",
394            bad.join("\n  ")
395        );
396    }
397
398    #[test]
399    fn halt_decodes_as_ecalli_zero() {
400        assert!(matches!(decode1(HALT), (Inst::Ecalli { imm: 0 }, 4)));
401    }
402
403    #[test]
404    fn curated_exact_fields() {
405        assert!(matches!(
406            decode1(add(10, 11, 12)),
407            (
408                Inst::Add {
409                    rd: 10,
410                    rs1: 11,
411                    rs2: 12
412                },
413                4
414            )
415        ));
416        assert!(matches!(
417            decode1(sub(5, 6, 7)),
418            (
419                Inst::Sub {
420                    rd: 5,
421                    rs1: 6,
422                    rs2: 7
423                },
424                4
425            )
426        ));
427        assert!(matches!(
428            decode1(div(10, 8, 9)),
429            (
430                Inst::Div {
431                    rd: 10,
432                    rs1: 8,
433                    rs2: 9
434                },
435                4
436            )
437        ));
438        assert!(matches!(
439            decode1(rem(10, 8, 9)),
440            (
441                Inst::Rem {
442                    rd: 10,
443                    rs1: 8,
444                    rs2: 9
445                },
446                4
447            )
448        ));
449        assert!(matches!(
450            decode1(mulhsu(10, 8, 9)),
451            (
452                Inst::Mulhsu {
453                    rd: 10,
454                    rs1: 8,
455                    rs2: 9
456                },
457                4
458            )
459        ));
460        assert!(matches!(
461            decode1(rori(5, 5, 5)),
462            (
463                Inst::Rori {
464                    rd: 5,
465                    rs1: 5,
466                    shamt: 5
467                },
468                4
469            )
470        ));
471        assert!(matches!(
472            decode1(slli(5, 5, 11)),
473            (
474                Inst::Slli {
475                    rd: 5,
476                    rs1: 5,
477                    shamt: 11
478                },
479                4
480            )
481        ));
482        assert!(matches!(
483            decode1(ld(7, 6, 8)),
484            (
485                Inst::Ld {
486                    rd: 7,
487                    rs1: 6,
488                    imm: 8
489                },
490                4
491            )
492        ));
493        assert!(matches!(
494            decode1(sd(6, 7, 0)),
495            (
496                Inst::Sd {
497                    rs1: 6,
498                    rs2: 7,
499                    imm: 0
500                },
501                4
502            )
503        ));
504        assert!(matches!(
505            decode1(addi(10, 0, -4)),
506            (
507                Inst::Addi {
508                    rd: 10,
509                    rs1: 0,
510                    imm: -4
511                },
512                4
513            )
514        ));
515        assert!(matches!(
516            decode1(lui(10, 0x12345)),
517            (
518                Inst::Lui {
519                    rd: 10,
520                    imm: 0x1234_5000
521                },
522                4
523            )
524        ));
525        assert!(matches!(
526            decode1(beq(8, 9, 12)),
527            (
528                Inst::Beq {
529                    rs1: 8,
530                    rs2: 9,
531                    imm: 12
532                },
533                4
534            )
535        ));
536    }
537
538    #[test]
539    fn li64_materializes_boundary_values() {
540        // The fold's li64 must reproduce arbitrary constants — check each
541        // word decodes (not Reserved) for a few boundary values.
542        for v in [
543            0u64,
544            1,
545            u64::MAX,
546            0x8000_0000_0000_0000,
547            0x7FFF_FFFF,
548            0xDEAD_BEEF_CAFE_F00D,
549        ] {
550            for w in li64(7, v) {
551                assert!(
552                    !matches!(decode1(w), (Inst::Reserved { .. }, _)),
553                    "li64({v:#018x}) produced Reserved word {w:#010x}",
554                );
555            }
556        }
557    }
558
559    #[test]
560    fn signature_epilogue_is_all_valid() {
561        let ep = signature_epilogue(0x1000_0000);
562        for w in &ep {
563            assert!(
564                !matches!(decode1(*w), (Inst::Reserved { .. }, _)),
565                "signature epilogue produced Reserved word {w:#010x}",
566            );
567        }
568        // li64 (the base address) + one `sd` per captured register.
569        assert_eq!(
570            ep.len(),
571            li64(SIG_BASE_REG, 0).len() + SIG_REGS,
572            "epilogue length"
573        );
574    }
575}