Skip to main content

javm_exec/
interp.rs

1//! PVM2 (RV+C+Zbb+Zba+Zbs+Zicond+custom-0) interpreter.
2//!
3//! [`Program`] bundles the constituents an interpreter run needs
4//! (code bytes, predecode output, code base) so the integration layer
5//! can cache the predecode alongside the bytecode and pass a single Arc
6//! to the executor.
7//!
8//! Mirrors the recompiler's semantics — same per-block gas charging at
9//! `Predecode::block_costs`, same RV-spec ALU/branch behaviour, same
10//! `Ecalli`/`Jalr` runtime contracts (jalr targets validated to be
11//! basic-block starts via the per-instruction block-start flag).
12//! Cross-checked against the recompiler in
13//! the `smoke` example: bit-identical `gas_used` and side-effects on
14//! every workload.
15//!
16//! Dispatch is a `match` over `Inst` variants. Instructions come from
17//! [`Predecode::insts`] (one entry per static instruction); static
18//! branch / jal targets are resolved to instruction indices via binary
19//! search on the (sorted) `insts` array. Reused infrastructure: `Regs`,
20//! `Memory` trait, `GasCounter`, `EcallHandler` — identical to the PVM
21//! interpreter's contract.
22
23use alloc::vec::Vec;
24
25use crate::ecall::{EcallHandler, EcallKind, EcallResult};
26use crate::exit::ExitReason;
27use crate::gas::GasCounter;
28use crate::instruction::Inst;
29use crate::mem::{Memory, perm};
30use crate::predecode::{Predecode, RvPreDecodedInst, predecode};
31use crate::regs::Regs;
32
33/// Predecoded PVM2 program: bytecode plus the per-instruction analysis
34/// the interpreter consumes. Cache-friendly — the integration layer
35/// builds one of these per Image and shares it across invocations.
36#[derive(Debug)]
37pub struct Program {
38    pub code: Vec<u8>,
39    pub predecode: Predecode,
40    /// Guest VA at which this code region is mapped, so that
41    /// `PC = code_base + byte_offset`. `regs.pc` and
42    /// `predecode.insts[].pc` are offsets; register-held code addresses
43    /// (return addresses, auipc results) are VAs (`code_base + offset`).
44    pub code_base: u32,
45}
46
47impl Program {
48    /// Predecode `code`. The predecode pass is O(code.len()); cache
49    /// the result. `code_base` is the guest VA the region is mapped at.
50    pub fn new(code: Vec<u8>, code_base: u32) -> Self {
51        let predecode = predecode(&code);
52        Self {
53            code,
54            predecode,
55            code_base,
56        }
57    }
58}
59
60/// PVM2 interpreter namespace.
61pub struct Interpreter;
62
63impl Interpreter {
64    /// Convenience wrapper for [`Interpreter::run`] that accepts a
65    /// cached [`Program`].
66    #[inline]
67    pub fn run_program<M: Memory>(
68        program: &Program,
69        regs: &mut Regs,
70        mem: &mut M,
71        gas: &mut GasCounter,
72        handler: &mut dyn EcallHandler,
73    ) -> ExitReason {
74        Self::run(
75            &program.predecode,
76            &program.code,
77            program.code_base,
78            regs,
79            mem,
80            gas,
81            handler,
82        )
83    }
84
85    /// Execute the predecoded PVM2 program starting at `regs.pc` (a
86    /// byte-offset into the code region). `code_base` is the guest VA
87    /// the region is mapped at: jal/jalr/auipc produce and consume
88    /// code addresses as `code_base + offset`.
89    pub fn run<M: Memory>(
90        predecode: &Predecode,
91        code: &[u8],
92        code_base: u32,
93        regs: &mut Regs,
94        mem: &mut M,
95        gas: &mut GasCounter,
96        handler: &mut dyn EcallHandler,
97    ) -> ExitReason {
98        // `decode_error_at` records a Reserved encoding seen during the
99        // predecode walk but doesn't preclude execution: programs may
100        // contain unreachable padding (e.g. `0x0000` bytes between
101        // functions) that the recompiler also tolerates. We panic only
102        // if execution actually *reaches* a `Inst::Reserved` arm.
103        let insts: &[RvPreDecodedInst] = &predecode.insts;
104        if insts.is_empty() {
105            return ExitReason::Panic;
106        }
107
108        // Resolve starting PC → instruction index. The entry must be a
109        // basic-block start: a fresh invocation enters at an endpoint's
110        // `entry_pc` (untrusted Image metadata) and every resume lands on
111        // a post-terminator `bb_start` (the pause invariant). A
112        // non-block-start entry would run a partial block having charged
113        // zero gas — so we Panic, exactly as the recompiler's prologue
114        // does (its dense dispatch table holds the panic stub at every
115        // non-block-start offset). Lazy: this fires at invocation, never
116        // at Image admission.
117        let mut idx = match find_idx_for_pc(insts, regs.pc as u32) {
118            Some(i) if insts[i].is_gas_block_start => i,
119            _ => return ExitReason::Panic,
120        };
121
122        loop {
123            let inst = unsafe { insts.get_unchecked(idx) };
124
125            // Per-block gas gate: **check before charge** (pre-reserve),
126            // so gas never goes negative and OOG never charges the
127            // un-entered block (see ~/docs/spec-staging/gas-cost.md §1).
128            // A block enters only if gas covers its instruction cost
129            // *plus* its worst-case #3 reserve; only the instruction cost
130            // is debited (the reserve is a gate). `cost == 0` marks an
131            // ecall/ecalli block — no static gate; charged dynamically.
132            if inst.is_gas_block_start {
133                let cost = predecode.block_costs[idx] as u64;
134                if cost != 0 {
135                    let reserve = predecode.block_reserves[idx] as u64;
136                    if gas.remaining() < cost + reserve {
137                        regs.pc = inst.pc as u64;
138                        return ExitReason::OutOfGas;
139                    }
140                    gas.charge(cost).expect("gas pre-reserved at block entry");
141                }
142            }
143
144            let pc = inst.pc;
145            let next_pc = inst.next_pc;
146
147            // Terminator arms below set `next_idx_override` to the target
148            // instruction index (computed from PC) and `break` out of
149            // the match so the loop loops with that idx.
150            let mut next_idx_override: Option<usize> = None;
151
152            match inst.inst {
153                // ---- Loads ---------------------------------------------------
154                Inst::Lb { rd, rs1, imm } => {
155                    let addr = compute_addr(regs, rs1, imm);
156                    if mem.touch_read(addr, 1, gas).is_err() {
157                        return page_fault(regs, pc, addr);
158                    }
159                    match load_u8(mem, code, code_base, addr) {
160                        Some(v) => reg_write(regs, rd, v as i8 as i64 as u64),
161                        None => return page_fault(regs, pc, addr),
162                    }
163                }
164                Inst::Lh { rd, rs1, imm } => {
165                    let addr = compute_addr(regs, rs1, imm);
166                    if mem.touch_read(addr, 2, gas).is_err() {
167                        return page_fault(regs, pc, addr);
168                    }
169                    match load_u16(mem, code, code_base, addr) {
170                        Some(v) => reg_write(regs, rd, v as i16 as i64 as u64),
171                        None => return page_fault(regs, pc, addr),
172                    }
173                }
174                Inst::Lw { rd, rs1, imm } => {
175                    let addr = compute_addr(regs, rs1, imm);
176                    if mem.touch_read(addr, 4, gas).is_err() {
177                        return page_fault(regs, pc, addr);
178                    }
179                    match load_u32(mem, code, code_base, addr) {
180                        Some(v) => reg_write(regs, rd, v as i32 as i64 as u64),
181                        None => return page_fault(regs, pc, addr),
182                    }
183                }
184                Inst::Ld { rd, rs1, imm } => {
185                    let addr = compute_addr(regs, rs1, imm);
186                    if mem.touch_read(addr, 8, gas).is_err() {
187                        return page_fault(regs, pc, addr);
188                    }
189                    match load_u64(mem, code, code_base, addr) {
190                        Some(v) => reg_write(regs, rd, v),
191                        None => return page_fault(regs, pc, addr),
192                    }
193                }
194                Inst::Lbu { rd, rs1, imm } => {
195                    let addr = compute_addr(regs, rs1, imm);
196                    if mem.touch_read(addr, 1, gas).is_err() {
197                        return page_fault(regs, pc, addr);
198                    }
199                    match load_u8(mem, code, code_base, addr) {
200                        Some(v) => reg_write(regs, rd, v as u64),
201                        None => return page_fault(regs, pc, addr),
202                    }
203                }
204                Inst::Lhu { rd, rs1, imm } => {
205                    let addr = compute_addr(regs, rs1, imm);
206                    if mem.touch_read(addr, 2, gas).is_err() {
207                        return page_fault(regs, pc, addr);
208                    }
209                    match load_u16(mem, code, code_base, addr) {
210                        Some(v) => reg_write(regs, rd, v as u64),
211                        None => return page_fault(regs, pc, addr),
212                    }
213                }
214                Inst::Lwu { rd, rs1, imm } => {
215                    let addr = compute_addr(regs, rs1, imm);
216                    if mem.touch_read(addr, 4, gas).is_err() {
217                        return page_fault(regs, pc, addr);
218                    }
219                    match load_u32(mem, code, code_base, addr) {
220                        Some(v) => reg_write(regs, rd, v as u64),
221                        None => return page_fault(regs, pc, addr),
222                    }
223                }
224
225                // ---- Stores --------------------------------------------------
226                Inst::Sb { rs1, rs2, imm } => {
227                    let addr = compute_addr(regs, rs1, imm);
228                    if mem.touch_write(addr, 1, gas).is_err()
229                        || !store_writable(mem, addr, 1)
230                        || !mem.write_u8(addr, reg_read(regs, rs2) as u8)
231                    {
232                        return page_fault(regs, pc, addr);
233                    }
234                }
235                Inst::Sh { rs1, rs2, imm } => {
236                    let addr = compute_addr(regs, rs1, imm);
237                    if mem.touch_write(addr, 2, gas).is_err()
238                        || !store_writable(mem, addr, 2)
239                        || !mem.write_u16_le(addr, reg_read(regs, rs2) as u16)
240                    {
241                        return page_fault(regs, pc, addr);
242                    }
243                }
244                Inst::Sw { rs1, rs2, imm } => {
245                    let addr = compute_addr(regs, rs1, imm);
246                    if mem.touch_write(addr, 4, gas).is_err()
247                        || !store_writable(mem, addr, 4)
248                        || !mem.write_u32_le(addr, reg_read(regs, rs2) as u32)
249                    {
250                        return page_fault(regs, pc, addr);
251                    }
252                }
253                Inst::Sd { rs1, rs2, imm } => {
254                    let addr = compute_addr(regs, rs1, imm);
255                    if mem.touch_write(addr, 8, gas).is_err()
256                        || !store_writable(mem, addr, 8)
257                        || !mem.write_u64_le(addr, reg_read(regs, rs2))
258                    {
259                        return page_fault(regs, pc, addr);
260                    }
261                }
262
263                // ---- ALU immediate (64-bit) ----------------------------------
264                Inst::Addi { rd, rs1, imm } => {
265                    let v = reg_read(regs, rs1).wrapping_add(imm as i64 as u64);
266                    reg_write(regs, rd, v);
267                }
268                Inst::Slti { rd, rs1, imm } => {
269                    let v = ((reg_read(regs, rs1) as i64) < (imm as i64)) as u64;
270                    reg_write(regs, rd, v);
271                }
272                Inst::Sltiu { rd, rs1, imm } => {
273                    let v = (reg_read(regs, rs1) < (imm as i64 as u64)) as u64;
274                    reg_write(regs, rd, v);
275                }
276                Inst::Andi { rd, rs1, imm } => {
277                    let v = reg_read(regs, rs1) & (imm as i64 as u64);
278                    reg_write(regs, rd, v);
279                }
280                Inst::Ori { rd, rs1, imm } => {
281                    let v = reg_read(regs, rs1) | (imm as i64 as u64);
282                    reg_write(regs, rd, v);
283                }
284                Inst::Xori { rd, rs1, imm } => {
285                    let v = reg_read(regs, rs1) ^ (imm as i64 as u64);
286                    reg_write(regs, rd, v);
287                }
288                Inst::Slli { rd, rs1, shamt } => {
289                    reg_write(
290                        regs,
291                        rd,
292                        reg_read(regs, rs1).wrapping_shl(shamt as u32 & 63),
293                    );
294                }
295                Inst::Srli { rd, rs1, shamt } => {
296                    reg_write(
297                        regs,
298                        rd,
299                        reg_read(regs, rs1).wrapping_shr(shamt as u32 & 63),
300                    );
301                }
302                Inst::Srai { rd, rs1, shamt } => {
303                    let v = (reg_read(regs, rs1) as i64).wrapping_shr(shamt as u32 & 63);
304                    reg_write(regs, rd, v as u64);
305                }
306
307                // ---- ALU immediate (32-bit, sign-extend to 64) ---------------
308                Inst::Addiw { rd, rs1, imm } => {
309                    let v = (reg_read(regs, rs1) as i32).wrapping_add(imm);
310                    reg_write(regs, rd, v as i64 as u64);
311                }
312                Inst::Slliw { rd, rs1, shamt } => {
313                    let v = (reg_read(regs, rs1) as u32).wrapping_shl(shamt as u32 & 31);
314                    reg_write(regs, rd, v as i32 as i64 as u64);
315                }
316                Inst::Srliw { rd, rs1, shamt } => {
317                    let v = (reg_read(regs, rs1) as u32).wrapping_shr(shamt as u32 & 31);
318                    reg_write(regs, rd, v as i32 as i64 as u64);
319                }
320                Inst::Sraiw { rd, rs1, shamt } => {
321                    let v = (reg_read(regs, rs1) as i32).wrapping_shr(shamt as u32 & 31);
322                    reg_write(regs, rd, v as i64 as u64);
323                }
324
325                // ---- ALU register-register (64-bit) --------------------------
326                Inst::Add { rd, rs1, rs2 } => {
327                    let v = reg_read(regs, rs1).wrapping_add(reg_read(regs, rs2));
328                    reg_write(regs, rd, v);
329                }
330                Inst::Sub { rd, rs1, rs2 } => {
331                    let v = reg_read(regs, rs1).wrapping_sub(reg_read(regs, rs2));
332                    reg_write(regs, rd, v);
333                }
334                Inst::Sll { rd, rs1, rs2 } => {
335                    let s = reg_read(regs, rs2) as u32 & 63;
336                    reg_write(regs, rd, reg_read(regs, rs1).wrapping_shl(s));
337                }
338                Inst::Srl { rd, rs1, rs2 } => {
339                    let s = reg_read(regs, rs2) as u32 & 63;
340                    reg_write(regs, rd, reg_read(regs, rs1).wrapping_shr(s));
341                }
342                Inst::Sra { rd, rs1, rs2 } => {
343                    let s = reg_read(regs, rs2) as u32 & 63;
344                    reg_write(
345                        regs,
346                        rd,
347                        (reg_read(regs, rs1) as i64).wrapping_shr(s) as u64,
348                    );
349                }
350                Inst::Slt { rd, rs1, rs2 } => {
351                    let v = ((reg_read(regs, rs1) as i64) < (reg_read(regs, rs2) as i64)) as u64;
352                    reg_write(regs, rd, v);
353                }
354                Inst::Sltu { rd, rs1, rs2 } => {
355                    let v = (reg_read(regs, rs1) < reg_read(regs, rs2)) as u64;
356                    reg_write(regs, rd, v);
357                }
358                Inst::Xor { rd, rs1, rs2 } => {
359                    reg_write(regs, rd, reg_read(regs, rs1) ^ reg_read(regs, rs2));
360                }
361                Inst::Or { rd, rs1, rs2 } => {
362                    reg_write(regs, rd, reg_read(regs, rs1) | reg_read(regs, rs2));
363                }
364                Inst::And { rd, rs1, rs2 } => {
365                    reg_write(regs, rd, reg_read(regs, rs1) & reg_read(regs, rs2));
366                }
367
368                // ---- ALU register-register (32-bit, sign-extend to 64) ------
369                Inst::Addw { rd, rs1, rs2 } => {
370                    let v = (reg_read(regs, rs1) as i32).wrapping_add(reg_read(regs, rs2) as i32);
371                    reg_write(regs, rd, v as i64 as u64);
372                }
373                Inst::Subw { rd, rs1, rs2 } => {
374                    let v = (reg_read(regs, rs1) as i32).wrapping_sub(reg_read(regs, rs2) as i32);
375                    reg_write(regs, rd, v as i64 as u64);
376                }
377                Inst::Sllw { rd, rs1, rs2 } => {
378                    let s = reg_read(regs, rs2) as u32 & 31;
379                    let v = (reg_read(regs, rs1) as u32).wrapping_shl(s);
380                    reg_write(regs, rd, v as i32 as i64 as u64);
381                }
382                Inst::Srlw { rd, rs1, rs2 } => {
383                    let s = reg_read(regs, rs2) as u32 & 31;
384                    let v = (reg_read(regs, rs1) as u32).wrapping_shr(s);
385                    reg_write(regs, rd, v as i32 as i64 as u64);
386                }
387                Inst::Sraw { rd, rs1, rs2 } => {
388                    let s = reg_read(regs, rs2) as u32 & 31;
389                    let v = (reg_read(regs, rs1) as i32).wrapping_shr(s);
390                    reg_write(regs, rd, v as i64 as u64);
391                }
392
393                // ---- M extension --------------------------------------------
394                Inst::Mul { rd, rs1, rs2 } => {
395                    let v = reg_read(regs, rs1).wrapping_mul(reg_read(regs, rs2));
396                    reg_write(regs, rd, v);
397                }
398                Inst::Mulh { rd, rs1, rs2 } => {
399                    let a = reg_read(regs, rs1) as i64 as i128;
400                    let b = reg_read(regs, rs2) as i64 as i128;
401                    reg_write(regs, rd, ((a * b) >> 64) as u64);
402                }
403                Inst::Mulhsu { rd, rs1, rs2 } => {
404                    let a = reg_read(regs, rs1) as i64 as i128;
405                    let b = reg_read(regs, rs2) as u128 as i128;
406                    reg_write(regs, rd, ((a * b) >> 64) as u64);
407                }
408                Inst::Mulhu { rd, rs1, rs2 } => {
409                    let a = reg_read(regs, rs1) as u128;
410                    let b = reg_read(regs, rs2) as u128;
411                    reg_write(regs, rd, ((a * b) >> 64) as u64);
412                }
413                Inst::Div { rd, rs1, rs2 } => {
414                    let a = reg_read(regs, rs1) as i64;
415                    let b = reg_read(regs, rs2) as i64;
416                    let v = if b == 0 {
417                        u64::MAX
418                    } else if a == i64::MIN && b == -1 {
419                        a as u64
420                    } else {
421                        (a / b) as u64
422                    };
423                    reg_write(regs, rd, v);
424                }
425                Inst::Divu { rd, rs1, rs2 } => {
426                    let a = reg_read(regs, rs1);
427                    let b = reg_read(regs, rs2);
428                    let v = a.checked_div(b).unwrap_or(u64::MAX);
429                    reg_write(regs, rd, v);
430                }
431                Inst::Rem { rd, rs1, rs2 } => {
432                    let a = reg_read(regs, rs1) as i64;
433                    let b = reg_read(regs, rs2) as i64;
434                    let v = if b == 0 {
435                        a as u64
436                    } else if a == i64::MIN && b == -1 {
437                        0
438                    } else {
439                        (a % b) as u64
440                    };
441                    reg_write(regs, rd, v);
442                }
443                Inst::Remu { rd, rs1, rs2 } => {
444                    let a = reg_read(regs, rs1);
445                    let b = reg_read(regs, rs2);
446                    let v = if b == 0 { a } else { a % b };
447                    reg_write(regs, rd, v);
448                }
449                Inst::Mulw { rd, rs1, rs2 } => {
450                    let v = (reg_read(regs, rs1) as i32).wrapping_mul(reg_read(regs, rs2) as i32);
451                    reg_write(regs, rd, v as i64 as u64);
452                }
453                Inst::Divw { rd, rs1, rs2 } => {
454                    let a = reg_read(regs, rs1) as i32;
455                    let b = reg_read(regs, rs2) as i32;
456                    let v = if b == 0 {
457                        u32::MAX as i32
458                    } else if a == i32::MIN && b == -1 {
459                        a
460                    } else {
461                        a / b
462                    };
463                    reg_write(regs, rd, v as i64 as u64);
464                }
465                Inst::Divuw { rd, rs1, rs2 } => {
466                    let a = reg_read(regs, rs1) as u32;
467                    let b = reg_read(regs, rs2) as u32;
468                    let v = a.checked_div(b).unwrap_or(u32::MAX);
469                    reg_write(regs, rd, v as i32 as i64 as u64);
470                }
471                Inst::Remw { rd, rs1, rs2 } => {
472                    let a = reg_read(regs, rs1) as i32;
473                    let b = reg_read(regs, rs2) as i32;
474                    let v = if b == 0 {
475                        a
476                    } else if a == i32::MIN && b == -1 {
477                        0
478                    } else {
479                        a % b
480                    };
481                    reg_write(regs, rd, v as i64 as u64);
482                }
483                Inst::Remuw { rd, rs1, rs2 } => {
484                    let a = reg_read(regs, rs1) as u32;
485                    let b = reg_read(regs, rs2) as u32;
486                    let v = if b == 0 { a } else { a % b };
487                    reg_write(regs, rd, v as i32 as i64 as u64);
488                }
489
490                // ---- Zbb -----------------------------------------------------
491                Inst::Clz { rd, rs1 } => {
492                    reg_write(regs, rd, reg_read(regs, rs1).leading_zeros() as u64);
493                }
494                Inst::Clzw { rd, rs1 } => {
495                    reg_write(
496                        regs,
497                        rd,
498                        (reg_read(regs, rs1) as u32).leading_zeros() as u64,
499                    );
500                }
501                Inst::Ctz { rd, rs1 } => {
502                    let v = reg_read(regs, rs1);
503                    let n = if v == 0 { 64 } else { v.trailing_zeros() };
504                    reg_write(regs, rd, n as u64);
505                }
506                Inst::Ctzw { rd, rs1 } => {
507                    let v = reg_read(regs, rs1) as u32;
508                    let n = if v == 0 { 32 } else { v.trailing_zeros() };
509                    reg_write(regs, rd, n as u64);
510                }
511                Inst::Cpop { rd, rs1 } => {
512                    reg_write(regs, rd, reg_read(regs, rs1).count_ones() as u64);
513                }
514                Inst::Cpopw { rd, rs1 } => {
515                    reg_write(regs, rd, (reg_read(regs, rs1) as u32).count_ones() as u64);
516                }
517                Inst::SextB { rd, rs1 } => {
518                    reg_write(regs, rd, reg_read(regs, rs1) as i8 as i64 as u64);
519                }
520                Inst::SextH { rd, rs1 } => {
521                    reg_write(regs, rd, reg_read(regs, rs1) as i16 as i64 as u64);
522                }
523                Inst::ZextH { rd, rs1 } => {
524                    reg_write(regs, rd, reg_read(regs, rs1) & 0xFFFF);
525                }
526                Inst::Rev8 { rd, rs1 } => {
527                    reg_write(regs, rd, reg_read(regs, rs1).swap_bytes());
528                }
529                Inst::OrcB { rd, rs1 } => {
530                    let v = reg_read(regs, rs1);
531                    // Per-byte: replace each byte by 0xFF if non-zero, else 0.
532                    let mut out: u64 = 0;
533                    for i in 0..8 {
534                        let b = (v >> (i * 8)) & 0xFF;
535                        if b != 0 {
536                            out |= 0xFFu64 << (i * 8);
537                        }
538                    }
539                    reg_write(regs, rd, out);
540                }
541                Inst::Min { rd, rs1, rs2 } => {
542                    let a = reg_read(regs, rs1) as i64;
543                    let b = reg_read(regs, rs2) as i64;
544                    reg_write(regs, rd, a.min(b) as u64);
545                }
546                Inst::Minu { rd, rs1, rs2 } => {
547                    let a = reg_read(regs, rs1);
548                    let b = reg_read(regs, rs2);
549                    reg_write(regs, rd, a.min(b));
550                }
551                Inst::Max { rd, rs1, rs2 } => {
552                    let a = reg_read(regs, rs1) as i64;
553                    let b = reg_read(regs, rs2) as i64;
554                    reg_write(regs, rd, a.max(b) as u64);
555                }
556                Inst::Maxu { rd, rs1, rs2 } => {
557                    let a = reg_read(regs, rs1);
558                    let b = reg_read(regs, rs2);
559                    reg_write(regs, rd, a.max(b));
560                }
561                Inst::Andn { rd, rs1, rs2 } => {
562                    reg_write(regs, rd, reg_read(regs, rs1) & !reg_read(regs, rs2));
563                }
564                Inst::Orn { rd, rs1, rs2 } => {
565                    reg_write(regs, rd, reg_read(regs, rs1) | !reg_read(regs, rs2));
566                }
567                Inst::Xnor { rd, rs1, rs2 } => {
568                    reg_write(regs, rd, !(reg_read(regs, rs1) ^ reg_read(regs, rs2)));
569                }
570                Inst::Rol { rd, rs1, rs2 } => {
571                    let s = reg_read(regs, rs2) as u32 & 63;
572                    reg_write(regs, rd, reg_read(regs, rs1).rotate_left(s));
573                }
574                Inst::Ror { rd, rs1, rs2 } => {
575                    let s = reg_read(regs, rs2) as u32 & 63;
576                    reg_write(regs, rd, reg_read(regs, rs1).rotate_right(s));
577                }
578                Inst::Rolw { rd, rs1, rs2 } => {
579                    let s = reg_read(regs, rs2) as u32 & 31;
580                    let v = (reg_read(regs, rs1) as u32).rotate_left(s);
581                    reg_write(regs, rd, v as i32 as i64 as u64);
582                }
583                Inst::Rorw { rd, rs1, rs2 } => {
584                    let s = reg_read(regs, rs2) as u32 & 31;
585                    let v = (reg_read(regs, rs1) as u32).rotate_right(s);
586                    reg_write(regs, rd, v as i32 as i64 as u64);
587                }
588                Inst::Rori { rd, rs1, shamt } => {
589                    reg_write(
590                        regs,
591                        rd,
592                        reg_read(regs, rs1).rotate_right(shamt as u32 & 63),
593                    );
594                }
595                Inst::Roriw { rd, rs1, shamt } => {
596                    let v = (reg_read(regs, rs1) as u32).rotate_right(shamt as u32 & 31);
597                    reg_write(regs, rd, v as i32 as i64 as u64);
598                }
599
600                // ---- Zba -----------------------------------------------------
601                Inst::Sh1add { rd, rs1, rs2 } => {
602                    let v = reg_read(regs, rs1)
603                        .wrapping_shl(1)
604                        .wrapping_add(reg_read(regs, rs2));
605                    reg_write(regs, rd, v);
606                }
607                Inst::Sh2add { rd, rs1, rs2 } => {
608                    let v = reg_read(regs, rs1)
609                        .wrapping_shl(2)
610                        .wrapping_add(reg_read(regs, rs2));
611                    reg_write(regs, rd, v);
612                }
613                Inst::Sh3add { rd, rs1, rs2 } => {
614                    let v = reg_read(regs, rs1)
615                        .wrapping_shl(3)
616                        .wrapping_add(reg_read(regs, rs2));
617                    reg_write(regs, rd, v);
618                }
619                Inst::Sh1adduw { rd, rs1, rs2 } => {
620                    let a = (reg_read(regs, rs1) as u32 as u64).wrapping_shl(1);
621                    reg_write(regs, rd, a.wrapping_add(reg_read(regs, rs2)));
622                }
623                Inst::Sh2adduw { rd, rs1, rs2 } => {
624                    let a = (reg_read(regs, rs1) as u32 as u64).wrapping_shl(2);
625                    reg_write(regs, rd, a.wrapping_add(reg_read(regs, rs2)));
626                }
627                Inst::Sh3adduw { rd, rs1, rs2 } => {
628                    let a = (reg_read(regs, rs1) as u32 as u64).wrapping_shl(3);
629                    reg_write(regs, rd, a.wrapping_add(reg_read(regs, rs2)));
630                }
631                Inst::Adduw { rd, rs1, rs2 } => {
632                    let a = reg_read(regs, rs1) as u32 as u64;
633                    reg_write(regs, rd, a.wrapping_add(reg_read(regs, rs2)));
634                }
635                Inst::Slliuw { rd, rs1, shamt } => {
636                    let a = reg_read(regs, rs1) as u32 as u64;
637                    reg_write(regs, rd, a.wrapping_shl(shamt as u32 & 63));
638                }
639
640                // ---- Zbs (single-bit) ----------------------------------------
641                Inst::Bclr { rd, rs1, rs2 } => {
642                    let bit = reg_read(regs, rs2) & 63;
643                    reg_write(regs, rd, reg_read(regs, rs1) & !(1u64 << bit));
644                }
645                Inst::Bset { rd, rs1, rs2 } => {
646                    let bit = reg_read(regs, rs2) & 63;
647                    reg_write(regs, rd, reg_read(regs, rs1) | (1u64 << bit));
648                }
649                Inst::Binv { rd, rs1, rs2 } => {
650                    let bit = reg_read(regs, rs2) & 63;
651                    reg_write(regs, rd, reg_read(regs, rs1) ^ (1u64 << bit));
652                }
653                Inst::Bext { rd, rs1, rs2 } => {
654                    let bit = reg_read(regs, rs2) & 63;
655                    reg_write(regs, rd, (reg_read(regs, rs1) >> bit) & 1);
656                }
657                Inst::Bclri { rd, rs1, shamt } => {
658                    reg_write(regs, rd, reg_read(regs, rs1) & !(1u64 << (shamt & 63)));
659                }
660                Inst::Bseti { rd, rs1, shamt } => {
661                    reg_write(regs, rd, reg_read(regs, rs1) | (1u64 << (shamt & 63)));
662                }
663                Inst::Binvi { rd, rs1, shamt } => {
664                    reg_write(regs, rd, reg_read(regs, rs1) ^ (1u64 << (shamt & 63)));
665                }
666                Inst::Bexti { rd, rs1, shamt } => {
667                    reg_write(regs, rd, (reg_read(regs, rs1) >> (shamt & 63)) & 1);
668                }
669
670                // ---- Zicond --------------------------------------------------
671                Inst::CzeroEqz { rd, rs1, rs2 } => {
672                    // (rs2 == 0) ? 0 : rs1
673                    let v = if reg_read(regs, rs2) == 0 {
674                        0
675                    } else {
676                        reg_read(regs, rs1)
677                    };
678                    reg_write(regs, rd, v);
679                }
680                Inst::CzeroNez { rd, rs1, rs2 } => {
681                    // (rs2 != 0) ? 0 : rs1
682                    let v = if reg_read(regs, rs2) != 0 {
683                        0
684                    } else {
685                        reg_read(regs, rs1)
686                    };
687                    reg_write(regs, rd, v);
688                }
689
690                // ---- Upper immediate ----------------------------------------
691                Inst::Lui { rd, imm } => {
692                    reg_write(regs, rd, imm as i64 as u64);
693                }
694                // auipc rd = pc_va + imm, where pc_va = code_base + pc.
695                // Folds to a constant the recompiler bakes in identically.
696                Inst::Auipc { rd, imm } => {
697                    let v = code_base.wrapping_add(pc).wrapping_add(imm as u32);
698                    reg_write(regs, rd, v as i32 as i64 as u64);
699                }
700
701                // ---- Control flow -------------------------------------------
702                Inst::Jal { rd, imm } => {
703                    if rd != 0 {
704                        // Return address is a guest VA (code_base + offset).
705                        reg_write(regs, rd, code_base.wrapping_add(next_pc) as u64);
706                    }
707                    let target = (pc as i64).wrapping_add(imm as i64) as u32;
708                    // The target must be a basic-block start (gas precharge
709                    // happens at block entry) — else Panic, matching the
710                    // recompiler's `emit_static_branch` panic stub and the
711                    // `Jalr` arm below. Accepting a mid-block target would
712                    // execute a partial block having charged zero gas.
713                    next_idx_override = Some(match find_idx_for_pc(insts, target) {
714                        Some(i) if insts[i].is_gas_block_start => i,
715                        _ => {
716                            regs.pc = pc as u64;
717                            return ExitReason::Panic;
718                        }
719                    });
720                }
721                // jalr rd, rs1, imm — indirect jump. target_va =
722                // (rs1 + imm) & 0xFFFFFFFF; offset = target_va - code_base.
723                // The target must be a basic-block start (gas precharge
724                // happens at block entry) — else Panic (security-critical:
725                // rejects mid-block / mid-instruction targets).
726                Inst::Jalr { rd, rs1, imm } => {
727                    let target_va = (reg_read(regs, rs1) as u32).wrapping_add(imm as u32);
728                    if rd != 0 {
729                        reg_write(regs, rd, code_base.wrapping_add(next_pc) as u64);
730                    }
731                    let target_off = target_va.wrapping_sub(code_base);
732                    next_idx_override = Some(match find_idx_for_pc(insts, target_off) {
733                        Some(i) if insts[i].is_gas_block_start => i,
734                        _ => {
735                            regs.pc = pc as u64;
736                            return ExitReason::Panic;
737                        }
738                    });
739                }
740                Inst::Beq { rs1, rs2, imm } => {
741                    if reg_read(regs, rs1) == reg_read(regs, rs2) {
742                        let target = (pc as i64).wrapping_add(imm as i64) as u32;
743                        match find_idx_for_pc(insts, target) {
744                            Some(i) if insts[i].is_gas_block_start => next_idx_override = Some(i),
745                            _ => {
746                                regs.pc = pc as u64;
747                                return ExitReason::Panic;
748                            }
749                        }
750                    }
751                }
752                Inst::Bne { rs1, rs2, imm } => {
753                    if reg_read(regs, rs1) != reg_read(regs, rs2) {
754                        let target = (pc as i64).wrapping_add(imm as i64) as u32;
755                        match find_idx_for_pc(insts, target) {
756                            Some(i) if insts[i].is_gas_block_start => next_idx_override = Some(i),
757                            _ => {
758                                regs.pc = pc as u64;
759                                return ExitReason::Panic;
760                            }
761                        }
762                    }
763                }
764                Inst::Blt { rs1, rs2, imm } => {
765                    if (reg_read(regs, rs1) as i64) < (reg_read(regs, rs2) as i64) {
766                        let target = (pc as i64).wrapping_add(imm as i64) as u32;
767                        match find_idx_for_pc(insts, target) {
768                            Some(i) if insts[i].is_gas_block_start => next_idx_override = Some(i),
769                            _ => {
770                                regs.pc = pc as u64;
771                                return ExitReason::Panic;
772                            }
773                        }
774                    }
775                }
776                Inst::Bge { rs1, rs2, imm } => {
777                    if (reg_read(regs, rs1) as i64) >= (reg_read(regs, rs2) as i64) {
778                        let target = (pc as i64).wrapping_add(imm as i64) as u32;
779                        match find_idx_for_pc(insts, target) {
780                            Some(i) if insts[i].is_gas_block_start => next_idx_override = Some(i),
781                            _ => {
782                                regs.pc = pc as u64;
783                                return ExitReason::Panic;
784                            }
785                        }
786                    }
787                }
788                Inst::Bltu { rs1, rs2, imm } => {
789                    if reg_read(regs, rs1) < reg_read(regs, rs2) {
790                        let target = (pc as i64).wrapping_add(imm as i64) as u32;
791                        match find_idx_for_pc(insts, target) {
792                            Some(i) if insts[i].is_gas_block_start => next_idx_override = Some(i),
793                            _ => {
794                                regs.pc = pc as u64;
795                                return ExitReason::Panic;
796                            }
797                        }
798                    }
799                }
800                Inst::Bgeu { rs1, rs2, imm } => {
801                    if reg_read(regs, rs1) >= reg_read(regs, rs2) {
802                        let target = (pc as i64).wrapping_add(imm as i64) as u32;
803                        match find_idx_for_pc(insts, target) {
804                            Some(i) if insts[i].is_gas_block_start => next_idx_override = Some(i),
805                            _ => {
806                                regs.pc = pc as u64;
807                                return ExitReason::Panic;
808                            }
809                        }
810                    }
811                }
812
813                // ---- System (no-op for our single-threaded VM) --------------
814                Inst::Fence | Inst::FenceI => {}
815
816                // ---- Custom-0 -----------------------------------------------
817                Inst::Trap => {
818                    regs.pc = pc as u64;
819                    return ExitReason::Trap;
820                }
821                Inst::EcallJar => {
822                    // ecall block: charge its dynamic cost (check-before-
823                    // charge). On OOG, gas is unchanged and the resume PC
824                    // is the ecall's OWN pc, so a top-up re-attempts the
825                    // whole op (gas-cost.md §3 "ecall/ecalli blocks").
826                    let cost = crate::gas_const::ecall_dynamic_cost(false);
827                    if gas.remaining() < cost {
828                        regs.pc = pc as u64;
829                        return ExitReason::OutOfGas;
830                    }
831                    gas.charge(cost).expect("ecall cost checked");
832                    regs.pc = next_pc as u64;
833                    match handler.handle(EcallKind::Ecall, regs, mem) {
834                        EcallResult::Continue => match find_idx_for_pc(insts, next_pc) {
835                            Some(i) => next_idx_override = Some(i),
836                            None => return ExitReason::Panic,
837                        },
838                        EcallResult::Exit(r) => return r,
839                    }
840                }
841                Inst::Ecalli { imm } => {
842                    let cost = crate::gas_const::ecall_dynamic_cost(true);
843                    if gas.remaining() < cost {
844                        regs.pc = pc as u64;
845                        return ExitReason::OutOfGas;
846                    }
847                    gas.charge(cost).expect("ecall cost checked");
848                    regs.pc = next_pc as u64;
849                    match handler.handle(EcallKind::Ecalli(imm as u32), regs, mem) {
850                        EcallResult::Continue => match find_idx_for_pc(insts, next_pc) {
851                            Some(i) => next_idx_override = Some(i),
852                            None => return ExitReason::Panic,
853                        },
854                        EcallResult::Exit(r) => return r,
855                    }
856                }
857                Inst::Fallthrough => {
858                    // Terminator no-op: just advance. The next instruction is
859                    // already marked as a block start so its cost gets
860                    // charged on the next iteration.
861                }
862
863                Inst::Reserved { .. } => {
864                    regs.pc = pc as u64;
865                    return ExitReason::Panic;
866                }
867            }
868
869            // Advance to the next instruction. Branches / Jal / Jalr /
870            // post-handler Ecalli set `next_idx_override`; everything else
871            // falls through to the sequential next.
872            match next_idx_override {
873                Some(new_idx) => idx = new_idx,
874                None => {
875                    idx += 1;
876                    if idx >= insts.len() {
877                        // Ran off the end. PVM2 expects every reachable
878                        // program path to end in a terminator.
879                        regs.pc = next_pc as u64;
880                        return ExitReason::Panic;
881                    }
882                }
883            }
884        }
885    }
886}
887
888// ----------------------------------------------------------------------------
889// Helpers
890// ----------------------------------------------------------------------------
891
892/// Read PVM2 register `x`, via the shared slot map ([`crate::regs`]).
893/// `x0` (and any reserved register `x16..x31`, which never reaches here once
894/// decode maps it to `Reserved`) reads as zero; `x1, x2, x5..x15` map to
895/// slots `0..12` and `x3`/`x4` to the spilled slots `13`/`14`.
896#[inline]
897fn reg_read(regs: &Regs, x: u8) -> u64 {
898    match crate::regs::REG_SLOT_LUT[(x & 31) as usize] {
899        0xFF => 0,
900        s => regs.gpr[s as usize],
901    }
902}
903
904/// Write PVM2 register `x`, via the shared slot map. Writes to `x0` (and
905/// any reserved register `x16..x31`) are no-ops; `x3`/`x4` write slots
906/// `13`/`14`.
907#[inline]
908fn reg_write(regs: &mut Regs, x: u8, v: u64) {
909    let s = crate::regs::REG_SLOT_LUT[(x & 31) as usize];
910    if s != 0xFF {
911        regs.gpr[s as usize] = v;
912    }
913}
914
915/// `(rs1 + imm) & 0xFFFFFFFF` — PVM2 effective address (sandbox is
916/// 32-bit). Matches `rv_addr_to_scratch` in the recompiler.
917#[inline]
918fn compute_addr(regs: &Regs, rs1: u8, imm: i32) -> u32 {
919    (reg_read(regs, rs1) as u32).wrapping_add(imm as u32)
920}
921
922/// Read a `width`-byte little-endian value (`width ≤ 8`, zero-extended
923/// into the `u64`) from the read-only code region if `addr` falls
924/// wholly within `[code_base, code_base + code.len())`. Returns `None`
925/// otherwise. Serves the PIC idiom (`auipc` + load) the spec blesses
926/// and the recompiler allows via its RO code direct-map: the
927/// interpreter's data buffer is based at `DATA_BASE`, so a code-region
928/// address never hits it and must be served from the code bytes here.
929#[inline]
930fn read_code(code: &[u8], code_base: u32, addr: u32, width: usize) -> Option<u64> {
931    let off = addr.checked_sub(code_base)? as usize;
932    let end = off.checked_add(width)?;
933    // The code region is page-rounded with a zero-padded tail — the
934    // recompiler maps whole pages, so it serves the last page's tail bytes
935    // as zero. Match that: serve real code bytes within `[0, code.len())`
936    // and zeros up to the page-rounded end; reject only accesses past it.
937    let rounded = (code.len() as u32).next_multiple_of(crate::mem::PAGE_SIZE) as usize;
938    if end > rounded {
939        return None;
940    }
941    let mut buf = [0u8; 8];
942    for (k, b) in buf.iter_mut().enumerate().take(width) {
943        let o = off + k;
944        if o < code.len() {
945            *b = code[o];
946        }
947    }
948    Some(u64::from_le_bytes(buf))
949}
950
951/// Width-typed loads that fall back to the read-only code region when
952/// the data buffer misses, so `auipc`+load of the guest's own code
953/// succeeds (B2 parity with the recompiler's RO code mapping).
954#[inline]
955fn load_u8<M: Memory>(mem: &M, code: &[u8], code_base: u32, addr: u32) -> Option<u8> {
956    mem.read_u8(addr)
957        .or_else(|| read_code(code, code_base, addr, 1).map(|v| v as u8))
958}
959#[inline]
960fn load_u16<M: Memory>(mem: &M, code: &[u8], code_base: u32, addr: u32) -> Option<u16> {
961    mem.read_u16_le(addr)
962        .or_else(|| read_code(code, code_base, addr, 2).map(|v| v as u16))
963}
964#[inline]
965fn load_u32<M: Memory>(mem: &M, code: &[u8], code_base: u32, addr: u32) -> Option<u32> {
966    mem.read_u32_le(addr)
967        .or_else(|| read_code(code, code_base, addr, 4).map(|v| v as u32))
968}
969#[inline]
970fn load_u64<M: Memory>(mem: &M, code: &[u8], code_base: u32, addr: u32) -> Option<u64> {
971    mem.read_u64_le(addr)
972        .or_else(|| read_code(code, code_base, addr, 8))
973}
974
975/// True iff every page a `width`-byte guest store at `addr` touches is
976/// writable (`perm::RW`). A `width`-byte store (`width ≤ 8`) spans at
977/// most two consecutive pages, so checking the first and last byte's
978/// page covers it. Enforced only for *guest* stores so the interpreter
979/// (the in-process consensus oracle) faults a write to a read-only /
980/// pinned page exactly as the recompiler's hardware page tables do —
981/// trusted kernel/host-call writes use `mem.write_*` directly and
982/// intentionally bypass this.
983#[inline]
984fn store_writable<M: Memory>(mem: &M, addr: u32, width: u32) -> bool {
985    mem.perm_of(addr) == perm::RW && mem.perm_of(addr.wrapping_add(width - 1)) == perm::RW
986}
987
988/// Build a `PageFault` exit and record the failing PC.
989#[inline]
990fn page_fault(regs: &mut Regs, pc: u32, addr: u32) -> ExitReason {
991    regs.pc = pc as u64;
992    ExitReason::PageFault(addr & !0xFFF)
993}
994
995/// Binary-search `insts` (sorted by `pc`) for an entry whose `pc` matches.
996#[inline]
997fn find_idx_for_pc(insts: &[RvPreDecodedInst], pc: u32) -> Option<usize> {
998    insts.binary_search_by_key(&pc, |i| i.pc).ok()
999}
1000
1001#[cfg(test)]
1002#[allow(clippy::identity_op)] // Encoding constants are clearer with explicit zero shifts.
1003mod tests {
1004    use super::*;
1005    use crate::ecall::PanickingHandler;
1006    use crate::mem::CopyingMemory;
1007    use crate::predecode::predecode;
1008    use alloc::vec::Vec;
1009
1010    fn enc4(words: &[u32]) -> Vec<u8> {
1011        let mut v = Vec::with_capacity(words.len() * 4);
1012        for w in words {
1013            v.extend_from_slice(&w.to_le_bytes());
1014        }
1015        v
1016    }
1017
1018    fn run_simple(code: &[u8], initial_gas: u64) -> (Regs, ExitReason, u64) {
1019        let pre = predecode(code);
1020        let mut regs = Regs::new();
1021        let mut mem = CopyingMemory::new();
1022        let mut gas = GasCounter::new(initial_gas);
1023        let mut h = PanickingHandler;
1024        let reason = Interpreter::run(&pre, code, 0, &mut regs, &mut mem, &mut gas, &mut h);
1025        let used = initial_gas.saturating_sub(gas.remaining());
1026        (regs, reason, used)
1027    }
1028
1029    #[test]
1030    fn trap_immediately() {
1031        // trap: custom-0 funct3=000 = opcode 0x0B = (0b00010 << 2) | 0b11
1032        // word = (0b000 << 12) | 0x0B = 0x0000_000B
1033        let code = enc4(&[0x0000_000B]);
1034        let (regs, reason, _) = run_simple(&code, 1_000_000);
1035        assert_eq!(reason, ExitReason::Trap);
1036        assert_eq!(regs.pc, 0);
1037    }
1038
1039    #[test]
1040    fn addi_then_trap() {
1041        // addi x10, x0, 42 ; trap
1042        // addi I-type: imm[11:0]=42, rs1=0, funct3=000, rd=10, opcode=0010011
1043        // = (42 << 20) | (0 << 15) | (0 << 12) | (10 << 7) | 0x13
1044        // = 0x02A00513
1045        let addi = (42u32 << 20) | (10 << 7) | 0x13;
1046        let trap = 0x0000_000Bu32;
1047        let code = enc4(&[addi, trap]);
1048        let (regs, reason, _) = run_simple(&code, 1_000_000);
1049        assert_eq!(reason, ExitReason::Trap);
1050        // x10 → slot 7 (x10 - 3 = 7)
1051        assert_eq!(regs.gpr[7], 42);
1052    }
1053
1054    #[test]
1055    fn div_by_zero_returns_neg_one() {
1056        // addi x5, x0, 7 ; addi x6, x0, 0 ; div x7, x5, x6 ; trap
1057        let addi_x5_7 = (7u32 << 20) | (5 << 7) | 0x13;
1058        let addi_x6_0 = (0u32 << 20) | (6 << 7) | 0x13;
1059        // div = funct7=0000001, rs2, rs1, funct3=100, rd, opcode=0110011
1060        let div = (1u32 << 25) | (6 << 20) | (5 << 15) | (0b100 << 12) | (7 << 7) | 0x33;
1061        let trap = 0x0000_000Bu32;
1062        let code = enc4(&[addi_x5_7, addi_x6_0, div, trap]);
1063        let (regs, _reason, _) = run_simple(&code, 1_000_000);
1064        // x7 → slot 4 (x7 - 3 = 4)
1065        assert_eq!(regs.gpr[4], u64::MAX);
1066    }
1067
1068    #[test]
1069    fn out_of_gas_at_block_start() {
1070        // addi x10, x0, 1 ; trap — needs more gas than supplied.
1071        let addi = (1u32 << 20) | (10 << 7) | 0x13;
1072        let trap = 0x0000_000Bu32;
1073        let code = enc4(&[addi, trap]);
1074        let (regs, reason, _) = run_simple(&code, 0);
1075        assert_eq!(reason, ExitReason::OutOfGas);
1076        assert_eq!(regs.pc, 0);
1077    }
1078
1079    #[test]
1080    fn sign_extend_addiw() {
1081        // addiw x10, x0, -1 → x10 = 0xFFFFFFFF_FFFFFFFF (sign-extended)
1082        let addiw = ((-1i32) as u32) << 20 | (0 << 15) | (0 << 12) | (10 << 7) | 0x1B;
1083        let trap = 0x0000_000Bu32;
1084        let code = enc4(&[addiw, trap]);
1085        let (regs, _reason, _) = run_simple(&code, 1_000_000);
1086        assert_eq!(regs.gpr[7], u64::MAX);
1087    }
1088
1089    #[test]
1090    fn czero_eqz_zeroes_when_rs2_is_zero() {
1091        // addi x5, x0, 42 ; addi x6, x0, 0 ; czero.eqz x7, x5, x6 ; trap
1092        // czero.eqz: funct7=0000111, rs2, rs1, funct3=101, rd, OP=0110011
1093        let addi_x5 = (42u32 << 20) | (5 << 7) | 0x13;
1094        let addi_x6 = (0u32 << 20) | (6 << 7) | 0x13;
1095        let czero = (0b0000111u32 << 25) | (6 << 20) | (5 << 15) | (0b101 << 12) | (7 << 7) | 0x33;
1096        let trap = 0x0000_000Bu32;
1097        let code = enc4(&[addi_x5, addi_x6, czero, trap]);
1098        let (regs, _reason, _) = run_simple(&code, 1_000_000);
1099        assert_eq!(regs.gpr[4], 0); // x7 = 0 because rs2 (x6) == 0
1100    }
1101
1102    #[test]
1103    fn czero_eqz_passes_when_rs2_nonzero() {
1104        let addi_x5 = (42u32 << 20) | (5 << 7) | 0x13;
1105        let addi_x6 = (3u32 << 20) | (6 << 7) | 0x13;
1106        let czero = (0b0000111u32 << 25) | (6 << 20) | (5 << 15) | (0b101 << 12) | (7 << 7) | 0x33;
1107        let trap = 0x0000_000Bu32;
1108        let code = enc4(&[addi_x5, addi_x6, czero, trap]);
1109        let (regs, _reason, _) = run_simple(&code, 1_000_000);
1110        assert_eq!(regs.gpr[4], 42); // rs2 != 0 → rd = rs1
1111    }
1112
1113    #[test]
1114    fn reserved_custom0_011_panics() {
1115        // custom-0 funct3=011 is reserved (was br_table); PVM2 uses
1116        // native jalr. Executing a Reserved encoding panics.
1117        let word = (0u32 << 20) | (5 << 15) | (0b011 << 12) | (0 << 7) | (0b00010 << 2) | 0b11;
1118        let code = enc4(&[word]);
1119        let pre = predecode(&code);
1120        let mut regs = Regs::new();
1121        let mut mem = CopyingMemory::new();
1122        let mut gas = GasCounter::new(1_000_000);
1123        let mut h = PanickingHandler;
1124        let reason = Interpreter::run(&pre, &code, 0, &mut regs, &mut mem, &mut gas, &mut h);
1125        assert_eq!(reason, ExitReason::Panic);
1126    }
1127
1128    // ---- B1: static jal/branch to a non-block-start must Panic ----------
1129    //
1130    // The block-start set is {0} ∪ {pc after a terminator}. A static
1131    // branch/jal whose target is a valid instruction boundary that is NOT
1132    // a block start would, unguarded, run a partial block having charged
1133    // zero gas. The interpreter must Panic there, matching the
1134    // recompiler's `emit_static_branch` panic stub. An honest linker never
1135    // emits such a target (it injects `fallthrough`), so this only bites a
1136    // crafted blob.
1137
1138    #[test]
1139    fn branch_to_non_block_start_panics() {
1140        // 0:  beq x0,x0,+8   (taken; terminator → PC=4 is a block start)
1141        // 4:  addi x10,x0,1  (not a terminator → PC=8 is NOT a block start)
1142        // 8:  addi x11,x0,2  (the branch target — mid-block)
1143        // 12: trap
1144        let beq = 0x0000_0463u32; // beq x0,x0,+8
1145        let addi1 = (1u32 << 20) | (10 << 7) | 0x13;
1146        let addi2 = (2u32 << 20) | (11 << 7) | 0x13;
1147        let trap = 0x0000_000Bu32;
1148        let code = enc4(&[beq, addi1, addi2, trap]);
1149        let (regs, reason, _) = run_simple(&code, 1_000_000);
1150        assert_eq!(reason, ExitReason::Panic);
1151        assert_eq!(regs.pc, 0); // the faulting branch's PC
1152    }
1153
1154    #[test]
1155    fn jal_to_non_block_start_panics() {
1156        // 0:  jal x0,+8      (terminator → PC=4 is a block start)
1157        // 4:  addi x10,x0,1  (not a terminator → PC=8 is NOT a block start)
1158        // 8:  addi x11,x0,2  (the jal target — mid-block)
1159        // 12: trap
1160        let jal = 0x0080_006Fu32; // jal x0,+8
1161        let addi1 = (1u32 << 20) | (10 << 7) | 0x13;
1162        let addi2 = (2u32 << 20) | (11 << 7) | 0x13;
1163        let trap = 0x0000_000Bu32;
1164        let code = enc4(&[jal, addi1, addi2, trap]);
1165        let (regs, reason, _) = run_simple(&code, 1_000_000);
1166        assert_eq!(reason, ExitReason::Panic);
1167        assert_eq!(regs.pc, 0);
1168    }
1169
1170    #[test]
1171    fn branch_to_real_block_start_is_allowed() {
1172        // Regression guard: a branch to a genuine block start still works.
1173        // 0:  beq x0,x0,+8   (terminator → PC=4 block start)
1174        // 4:  trap           (terminator → PC=8 IS a block start; unreached)
1175        // 8:  addi x10,x0,7  (the branch target — a valid block start)
1176        // 12: trap
1177        let beq = 0x0000_0463u32; // beq x0,x0,+8
1178        let trap = 0x0000_000Bu32;
1179        let addi = (7u32 << 20) | (10 << 7) | 0x13;
1180        let code = enc4(&[beq, trap, addi, trap]);
1181        let (regs, reason, _) = run_simple(&code, 1_000_000);
1182        assert_eq!(reason, ExitReason::Trap);
1183        assert_eq!(regs.gpr[7], 7); // x10 → slot 7: the branch was taken
1184    }
1185
1186    // ---- B2: a guest can read (but not write) its own code bytes --------
1187
1188    #[test]
1189    fn auipc_load_reads_own_code_region() {
1190        // auipc x10,0 ; lw x11,0(x10) ; trap. With the data buffer based
1191        // away from the code region, the load address lands in the code
1192        // region and must be served from the code bytes (the PIC idiom),
1193        // not page-fault — matching the recompiler's RO code direct-map.
1194        let auipc = (0u32 << 12) | (10 << 7) | 0x17; // auipc x10,0
1195        let lw = (0u32 << 20) | (10 << 15) | (0b010 << 12) | (11 << 7) | 0x03; // lw x11,0(x10)
1196        let trap = 0x0000_000Bu32;
1197        let code = enc4(&[auipc, lw, trap]);
1198        let pre = predecode(&code);
1199        let mut regs = Regs::new();
1200        let mut mem = CopyingMemory::new();
1201        mem.base = 0x1000_0000; // data buffer elsewhere; code region unmapped here
1202        let mut gas = GasCounter::new(1_000_000);
1203        let mut h = PanickingHandler;
1204        let code_base = 0x0040_0000u32;
1205        let reason = Interpreter::run(
1206            &pre, &code, code_base, &mut regs, &mut mem, &mut gas, &mut h,
1207        );
1208        assert_eq!(reason, ExitReason::Trap);
1209        assert_eq!(regs.gpr[7], code_base as u64); // x10 = code_base + 0
1210        assert_eq!(regs.gpr[8] as u32, auipc); // x11 = the first code word
1211    }
1212
1213    #[test]
1214    fn store_into_code_region_faults() {
1215        // auipc x10,0 ; sw x0,0(x10) ; trap. Code is read-only: a store
1216        // into the code region must PageFault in both engines.
1217        let auipc = (0u32 << 12) | (10 << 7) | 0x17; // auipc x10,0
1218        let sw = (0u32 << 25) | (0 << 20) | (10 << 15) | (0b010 << 12) | (0 << 7) | 0x23; // sw x0,0(x10)
1219        let trap = 0x0000_000Bu32;
1220        let code = enc4(&[auipc, sw, trap]);
1221        let pre = predecode(&code);
1222        let mut regs = Regs::new();
1223        let mut mem = CopyingMemory::new();
1224        mem.base = 0x1000_0000;
1225        let mut gas = GasCounter::new(1_000_000);
1226        let mut h = PanickingHandler;
1227        let code_base = 0x0040_0000u32;
1228        let reason = Interpreter::run(
1229            &pre, &code, code_base, &mut regs, &mut mem, &mut gas, &mut h,
1230        );
1231        assert_eq!(reason, ExitReason::PageFault(code_base));
1232    }
1233
1234    // ---- B3: a guest store to a read-only page faults -------------------
1235
1236    #[test]
1237    fn store_writable_respects_per_page_perms() {
1238        // Two RW pages, then mark the second read-only. A store wholly in
1239        // an RW page passes; one touching the RO page (incl. a straddle)
1240        // fails.
1241        let page = crate::mem::PAGE_SIZE;
1242        let mut mem = CopyingMemory::with_pages(2, perm::RW);
1243        mem.perms[1] = perm::RO;
1244        assert!(store_writable(&mem, 0, 4)); // wholly in RW page
1245        assert!(!store_writable(&mem, page, 4)); // wholly in RO page
1246        assert!(!store_writable(&mem, page - 2, 4)); // straddles RW→RO
1247    }
1248
1249    #[test]
1250    fn guest_store_to_ro_page_faults() {
1251        // lui x5,1 (x5 = 0x1000) ; sw x0,0(x5) ; trap. Page 1 is RO, so
1252        // the store faults; the recompiler's hardware RO mapping faults
1253        // identically.
1254        let lui = (1u32 << 12) | (5 << 7) | 0x37; // lui x5,1
1255        let sw = (0u32 << 25) | (0 << 20) | (5 << 15) | (0b010 << 12) | (0 << 7) | 0x23; // sw x0,0(x5)
1256        let trap = 0x0000_000Bu32;
1257        let code = enc4(&[lui, sw, trap]);
1258        let pre = predecode(&code);
1259        let mut regs = Regs::new();
1260        let mut mem = CopyingMemory::with_pages(2, perm::RW);
1261        mem.perms[1] = perm::RO;
1262        let mut gas = GasCounter::new(1_000_000);
1263        let mut h = PanickingHandler;
1264        let reason = Interpreter::run(&pre, &code, 0, &mut regs, &mut mem, &mut gas, &mut h);
1265        assert_eq!(reason, ExitReason::PageFault(crate::mem::PAGE_SIZE));
1266        // The RO page was not mutated.
1267        assert_eq!(mem.read_u32_le(crate::mem::PAGE_SIZE), Some(0));
1268    }
1269
1270    // ---- B6: entry PC must be a basic-block start ----------------------
1271
1272    #[test]
1273    fn entry_at_non_block_start_panics() {
1274        // Enter at PC=4, which is mid-block: the addi at PC=0 is not a
1275        // terminator, so PC=4 is not a block start. Must Panic at
1276        // invocation, matching the recompiler's dispatch-table prologue
1277        // (a non-block-start offset holds the panic stub).
1278        let addi1 = (1u32 << 20) | (10 << 7) | 0x13;
1279        let addi2 = (2u32 << 20) | (11 << 7) | 0x13;
1280        let trap = 0x0000_000Bu32;
1281        let code = enc4(&[addi1, addi2, trap]);
1282        let pre = predecode(&code);
1283        let mut regs = Regs::new();
1284        regs.pc = 4; // non-block-start entry
1285        let mut mem = CopyingMemory::new();
1286        let mut gas = GasCounter::new(1_000_000);
1287        let mut h = PanickingHandler;
1288        let reason = Interpreter::run(&pre, &code, 0, &mut regs, &mut mem, &mut gas, &mut h);
1289        assert_eq!(reason, ExitReason::Panic);
1290    }
1291
1292    #[test]
1293    fn entry_at_real_block_start_is_allowed() {
1294        // 0: trap (terminator → PC=4 is a block start)
1295        // 4: addi x10,x0,7 ; 8: trap. Entering at the PC=4 block start runs.
1296        let trap = 0x0000_000Bu32;
1297        let addi = (7u32 << 20) | (10 << 7) | 0x13;
1298        let code = enc4(&[trap, addi, trap]);
1299        let pre = predecode(&code);
1300        let mut regs = Regs::new();
1301        regs.pc = 4; // post-terminator block start
1302        let mut mem = CopyingMemory::new();
1303        let mut gas = GasCounter::new(1_000_000);
1304        let mut h = PanickingHandler;
1305        let reason = Interpreter::run(&pre, &code, 0, &mut regs, &mut mem, &mut gas, &mut h);
1306        assert_eq!(reason, ExitReason::Trap);
1307        assert_eq!(regs.gpr[7], 7); // x10 → slot 7: PC=4 block executed
1308    }
1309
1310    // ---- x3/x4 are real (spilled) registers the interpreter executes ----
1311
1312    #[test]
1313    fn x3_x4_execute_as_real_registers() {
1314        // addi x3, x0, 7 ; addi x4, x0, 5 ; add x5, x3, x4 ; trap.
1315        // x3/x4 are RV64E GPRs (high slots 13/14); the interpreter executes
1316        // them, so x5 = 7 + 5 = 12.
1317        let addi_x3_7 = (7u32 << 20) | (3 << 7) | 0x13; // addi x3, x0, 7
1318        let addi_x4_5 = (5u32 << 20) | (4 << 7) | 0x13; // addi x4, x0, 5
1319        let add_x5 = (4u32 << 20) | (3 << 15) | (5 << 7) | 0x33; // add x5, x3, x4
1320        let trap = 0x0000_000Bu32;
1321        let code = enc4(&[addi_x3_7, addi_x4_5, add_x5, trap]);
1322        let (regs, reason, _) = run_simple(&code, 1_000_000);
1323        assert_eq!(reason, ExitReason::Trap);
1324        // x5 → slot 2 (x5 - 3); x3 → slot 13; x4 → slot 14.
1325        assert_eq!(regs.gpr[2], 12);
1326        assert_eq!(regs.gpr[13], 7);
1327        assert_eq!(regs.gpr[14], 5);
1328    }
1329
1330    #[test]
1331    fn x3_free_arithmetic_still_runs() {
1332        // add x5, x6, x7 ; trap — no x3/x4, executes normally.
1333        let add_ok = 0x0073_02B3u32; // add x5, x6, x7
1334        let trap = 0x0000_000Bu32;
1335        let code = enc4(&[add_ok, trap]);
1336        let (_regs, reason, _) = run_simple(&code, 1_000_000);
1337        assert_eq!(reason, ExitReason::Trap);
1338    }
1339}