Skip to main content

javm_transpiler/
riscv.rs

1//! RISC-V instruction decoder and PVM instruction translator.
2//!
3//! Decodes rv32em/rv64em instructions and translates them to equivalent
4//! PVM bytecode sequences.
5
6use crate::TranspileError;
7
8/// RISC-V register to PVM register mapping.
9///
10/// RISC-V has 16 registers in the `e` (embedded) ABI:
11///   x0 (zero), x1 (ra), x2 (sp), x3 (gp), x4 (tp),
12///   x5 (t0), x6 (t1), x7 (t2), x8 (s0), x9 (s1),
13///   x10 (a0), x11 (a1), x12 (a2), x13 (a3), x14 (a4), x15 (a5)
14///
15/// PVM has 13 registers (0-12):
16///   0=RA, 1=SP, 2=T0, 3=T1, 4=T2, 5=S0, 6=S1,
17///   7=A0, 8=A1, 9=A2, 10=A3, 11=A4, 12=A5
18///
19/// Mapping: x0 → zero (special), x1 → 0, x2 → 1, x5-x15 → 2-12
20/// x3 (gp) and x4 (tp) have no direct mapping and must be spilled.
21fn map_register(rv_reg: u8) -> Result<Option<u8>, TranspileError> {
22    match rv_reg {
23        0 => Ok(None),                                         // x0 = zero register (always 0)
24        1 => Ok(Some(0)),                                      // x1 (ra) → PVM reg 0 (RA)
25        2 => Ok(Some(1)),                                      // x2 (sp) → PVM reg 1 (SP)
26        3 | 4 => Err(TranspileError::RegisterMapping(rv_reg)), // gp, tp: no mapping
27        5 => Ok(Some(2)),                                      // x5 (t0) → PVM reg 2 (T0)
28        6 => Ok(Some(3)),                                      // x6 (t1) → PVM reg 3 (T1)
29        7 => Ok(Some(4)),                                      // x7 (t2) → PVM reg 4 (T2)
30        8 => Ok(Some(5)),                                      // x8 (s0) → PVM reg 5 (S0)
31        9 => Ok(Some(6)),                                      // x9 (s1) → PVM reg 6 (S1)
32        10 => Ok(Some(7)),                                     // x10 (a0) → PVM reg 7 (A0)
33        11 => Ok(Some(8)),                                     // x11 (a1) → PVM reg 8 (A1)
34        12 => Ok(Some(9)),                                     // x12 (a2) → PVM reg 9 (A2)
35        13 => Ok(Some(10)),                                    // x13 (a3) → PVM reg 10 (A3)
36        14 => Ok(Some(11)),                                    // x14 (a4) → PVM reg 11 (A4)
37        15 => Ok(Some(12)),                                    // x15 (a5) → PVM reg 12 (A5)
38        _ => Err(TranspileError::RegisterMapping(rv_reg)),
39    }
40}
41
42/// Determine the minimum byte width for encoding a signed immediate in PVM format.
43///
44/// Returns `(lx, bytes)` where `lx` is the byte count (0, 1, 2, or 4) and `bytes`
45/// are the little-endian encoded value.
46fn encode_var_imm(imm: i32) -> (u8, Vec<u8>) {
47    if imm == 0 {
48        (0, vec![])
49    } else if (-128..=127).contains(&imm) {
50        (1, vec![imm as i8 as u8])
51    } else if (-32768..=32767).contains(&imm) {
52        (2, (imm as i16).to_le_bytes().to_vec())
53    } else {
54        (4, imm.to_le_bytes().to_vec())
55    }
56}
57
58/// Translation context for converting RISC-V to PVM.
59pub struct TranslationContext {
60    /// Emitted PVM code bytes.
61    pub code: Vec<u8>,
62    /// Bitmask: 1 for instruction start, 0 for continuation.
63    pub bitmask: Vec<u8>,
64    /// Jump table entries.
65    pub jump_table: Vec<u32>,
66    /// Whether translating 64-bit RISC-V.
67    pub is_64bit: bool,
68    /// Map from RISC-V address to PVM code offset.
69    pub address_map: std::collections::HashMap<u64, u32>,
70    /// Pending branch fixups: (pvm_imm_offset, target_rv_address, fixup_size)
71    fixups: Vec<(usize, u64, u8)>,
72    /// Map from fixup imm offset → instruction PC (for PC-relative encoding)
73    fixup_pcs: std::collections::HashMap<usize, u32>,
74    /// Return-address fixups: (jump_table_index, risc-v return address).
75    /// Resolved during `apply_fixups` to patch jump table entries.
76    pub(crate) return_fixups: Vec<(usize, u64)>,
77    /// Pending AUIPC: (rd, computed_address). Used to pair with the next JALR.
78    pending_auipc: Option<(u8, u64)>,
79    /// Pending LUI: (rd, upper_imm). Used to fuse LUI+ADDI into single load_imm.
80    pending_lui: Option<(u8, i64)>,
81    /// Last emitted load_imm: (rd, value, code_position_before_emit).
82    /// Enables fusion with a subsequent ADD/AND/OR/XOR/load/store into the
83    /// immediate form, eliminating the load_imm instruction entirely.
84    pub(crate) pending_load_imm: Option<(u8, i64, usize)>,
85    /// Last immediate loaded into t0 (x5) — used for ecalli cap slot.
86    last_t0_imm: Option<i32>,
87    /// CSR marker for ecall/ecalli distinction.
88    /// 0x800 = next ecall → PVM ecall, 0x801 = next ecall → PVM ecalli.
89    ecall_marker: Option<u32>,
90    /// RISC-V code address ranges (lo, hi) — used to detect function pointers
91    /// loaded via auipc+addi that need jump table entries.
92    pub code_ranges: Vec<(u64, u64)>,
93}
94
95impl TranslationContext {
96    pub fn new(is_64bit: bool) -> Self {
97        Self {
98            code: Vec::new(),
99            bitmask: Vec::new(),
100            jump_table: Vec::new(),
101            is_64bit,
102            address_map: std::collections::HashMap::new(),
103            fixups: Vec::new(),
104            fixup_pcs: std::collections::HashMap::new(),
105            return_fixups: Vec::new(),
106            pending_auipc: None,
107            pending_lui: None,
108            pending_load_imm: None,
109            last_t0_imm: None,
110            ecall_marker: None,
111            code_ranges: Vec::new(),
112        }
113    }
114
115    /// Flush any pending buffered instructions (LUI, AUIPC) at section boundaries.
116    pub(crate) fn flush_pending(&mut self) -> Result<(), TranspileError> {
117        if let Some((auipc_rd, auipc_val)) = self.pending_auipc.take() {
118            self.emit_load_imm(auipc_rd, auipc_val as i64)?;
119        }
120        if let Some((lui_rd, lui_val)) = self.pending_lui.take() {
121            self.emit_load_imm(lui_rd, lui_val)?;
122        }
123        // pending_load_imm is already emitted — just clear the tracking
124        if self.pending_load_imm.is_some() {
125            self.pending_load_imm = None;
126        }
127        Ok(())
128    }
129
130    /// Translate one or more 32-bit RISC-V instructions starting at `offset`.
131    /// Returns the number of bytes consumed (always 4).
132    pub(crate) fn translate_instruction(
133        &mut self,
134        section: &[u8],
135        offset: usize,
136        base: u64,
137    ) -> Result<usize, TranspileError> {
138        let inst = u32::from_le_bytes([
139            section[offset],
140            section[offset + 1],
141            section[offset + 2],
142            section[offset + 3],
143        ]);
144        let addr = base + offset as u64;
145        self.translate_one(inst, addr)?;
146        Ok(4)
147    }
148
149    /// Translate a single 32-bit RISC-V instruction.
150    fn translate_one(&mut self, inst: u32, _addr: u64) -> Result<(), TranspileError> {
151        let opcode = inst & 0x7F;
152        let rd = ((inst >> 7) & 0x1F) as u8;
153        let funct3 = (inst >> 12) & 0x7;
154        let rs1 = ((inst >> 15) & 0x1F) as u8;
155        let rs2 = ((inst >> 20) & 0x1F) as u8;
156        let funct7 = (inst >> 25) & 0x7F;
157
158        // Flush pending auipc if this isn't a JALR or OP-IMM (ADDI) that consumes it.
159        if opcode != 0x67
160            && opcode != 0x13
161            && let Some((auipc_rd, auipc_val)) = self.pending_auipc.take()
162        {
163            self.emit_load_imm(auipc_rd, auipc_val as i64)?;
164        }
165
166        // Flush pending LUI if this isn't an OP-IMM (ADDI) that consumes it.
167        if opcode != 0x13
168            && let Some((lui_rd, lui_val)) = self.pending_lui.take()
169        {
170            self.emit_load_imm(lui_rd, lui_val)?;
171        }
172
173        // Clear pending_load_imm if this isn't an instruction that can consume it.
174        // OP (0x33), OP-32 (0x3B), Branch (0x63), Store (0x23), and Load (0x03)
175        // handlers check and potentially fuse.
176        if opcode != 0x33 && opcode != 0x3B && opcode != 0x63 && opcode != 0x23 && opcode != 0x03 {
177            self.pending_load_imm = None; // already emitted, just clear tracking
178        }
179
180        match opcode {
181            0x37 => {
182                // LUI — buffer for potential LUI+ADDI fusion
183                let imm = (inst & 0xFFFFF000) as i32;
184                // Flush any previous pending LUI (consecutive LUIs)
185                if let Some((prev_rd, prev_val)) = self.pending_lui.take() {
186                    self.emit_load_imm(prev_rd, prev_val)?;
187                }
188                self.pending_lui = Some((rd, imm as i64));
189            }
190            0x17 => {
191                // AUIPC — PC + upper immediate
192                let imm = (inst & 0xFFFFF000) as i32;
193                let computed = (_addr as i64 + imm as i64) as u64;
194                // Record for pairing with the next JALR instruction.
195                // Don't emit anything yet — the JALR handler will use this.
196                self.pending_auipc = Some((rd, computed));
197            }
198            0x6F => {
199                // JAL
200                let imm = decode_j_imm(inst);
201                let target = (_addr as i64 + imm as i64) as u64;
202                if rd == 0 {
203                    // Plain jump (tail call / goto)
204                    self.emit_jump(target);
205                } else {
206                    // Function call: fused load_imm_jump (opcode 80)
207                    let rv_return_addr = _addr + 4;
208                    self.emit_call(rd, rv_return_addr, target)?;
209                }
210            }
211            0x67 => {
212                // JALR
213                match funct3 {
214                    0 => {
215                        let imm = (inst as i32) >> 20;
216                        self.translate_jalr(rd, rs1, imm, _addr)?;
217                    }
218                    _ => {
219                        return Err(TranspileError::UnsupportedInstruction {
220                            offset: _addr as usize,
221                            detail: format!("JALR funct3={}", funct3),
222                        });
223                    }
224                }
225            }
226            0x63 => {
227                // Branch
228                let imm = decode_b_imm(inst);
229                let target = (_addr as i64 + imm as i64) as u64;
230                self.translate_branch(funct3, rs1, rs2, target)?;
231            }
232            0x03 => {
233                // Load
234                let imm = (inst as i32) >> 20;
235                self.translate_load(funct3, rd, rs1, imm)?;
236            }
237            0x23 => {
238                // Store
239                let imm = decode_s_imm(inst);
240                self.translate_store(funct3, rs1, rs2, imm)?;
241            }
242            0x13 => {
243                // OP-IMM (add_i, xor_i, etc.)
244                let imm = (inst as i32) >> 20;
245                self.translate_op_imm(funct3, funct7, rd, rs1, imm)?;
246            }
247            0x33 => {
248                // OP (add, sub, mul, etc.)
249                self.translate_op(funct3, funct7, rd, rs1, rs2, _addr)?;
250            }
251            0x1B => {
252                // OP-IMM-32 (addiw, slliw, etc.) — RV64 only
253                let imm = (inst as i32) >> 20;
254                self.translate_op_imm_32(funct3, funct7, rd, rs1, imm)?;
255            }
256            0x3B => {
257                // OP-32 (addw, subw, etc.) — RV64 only
258                self.translate_op_32(funct3, funct7, rd, rs1, rs2)?;
259            }
260            0x73 => {
261                // SYSTEM
262                match funct3 {
263                    0 => {
264                        let csr = (inst >> 20) & 0xFFF;
265                        match csr {
266                            0 => {
267                                // ECALL — dispatch based on CSR marker
268                                match self.ecall_marker.take() {
269                                    Some(0x800) => {
270                                        // CSR 0x800 → PVM ecall (management ops)
271                                        self.emit_ecall();
272                                    }
273                                    Some(0x801) => {
274                                        // CSR 0x801 → PVM ecalli (CALL a cap)
275                                        let id = self.last_t0_imm.unwrap_or(0) as u32;
276                                        self.emit_ecalli(id);
277                                        self.last_t0_imm = None;
278                                    }
279                                    _ => {
280                                        // No marker (legacy) — treat as ecalli for backward compat
281                                        let id = self.last_t0_imm.unwrap_or(0) as u32;
282                                        self.emit_ecalli(id);
283                                        self.last_t0_imm = None;
284                                    }
285                                }
286                            }
287                            1 => self.emit_inst(0), // EBREAK → trap
288                            _ => self.emit_inst(0), // unimp/unknown CSR → trap
289                        }
290                    }
291                    1 => {
292                        // CSRRW: check for custom markers 0x800, 0x801
293                        let csr = (inst >> 20) & 0xFFF;
294                        match csr {
295                            0x800 | 0x801 => {
296                                // Custom CSR marker for ecall/ecalli distinction
297                                self.ecall_marker = Some(csr);
298                                // Don't emit any PVM instruction — marker consumed on next ecall
299                            }
300                            _ => self.emit_inst(0), // unknown CSR → trap
301                        }
302                    }
303                    _ => self.emit_inst(0), // other CSR ops → trap
304                }
305            }
306            0x0F => {
307                // FENCE
308                self.emit_inst(1); // → fallthrough (nop)
309            }
310            0x0B => {
311                // CUSTOM-0 — T-Head extensions
312                match (funct7, funct3) {
313                    (0x20, 1) => {
314                        // th.mveqz rd, rs1, rs2: if rs2 == 0 then rd = rs1
315                        if rd == 0 { /* nop */
316                        } else if rs2 == 0 {
317                            // Condition is x0 (always 0) → always execute: rd = rs1
318                            if rs1 == 0 {
319                                self.emit_load_imm(rd, 0)?;
320                            } else if rd == rs1 {
321                                // Self-move is a nop
322                                self.emit_inst(1); // fallthrough
323                            } else {
324                                let pvm_rd = self.require_reg(rd)?;
325                                let pvm_rs1 = self.require_reg(rs1)?;
326                                self.emit_inst(100); // move_reg
327                                self.emit_data(pvm_rd | (pvm_rs1 << 4));
328                            }
329                        } else if rs1 == 0 {
330                            // Source is x0 (always 0): if rs2 == 0 then rd = 0
331                            let pvm_rd = self.require_reg(rd)?;
332                            let pvm_rs2 = self.require_reg(rs2)?;
333                            self.emit_inst(147); // CmovIzImm
334                            self.emit_data(pvm_rd | (pvm_rs2 << 4));
335                            self.emit_var_imm(0);
336                        } else {
337                            let pvm_rd = self.require_reg(rd)?;
338                            let pvm_rs1 = self.require_reg(rs1)?;
339                            let pvm_rs2 = self.require_reg(rs2)?;
340                            self.emit_inst(218); // CmovIz
341                            self.emit_data(pvm_rs1 | (pvm_rs2 << 4));
342                            self.emit_data(pvm_rd);
343                        }
344                    }
345                    (0x21, 1) => {
346                        // th.mvnez rd, rs1, rs2: if rs2 != 0 then rd = rs1
347                        if rd == 0 || rs2 == 0 {
348                            // rd==0: nop. rs2==x0: condition "x0 != 0" is always false → nop
349                            self.emit_inst(1); // fallthrough
350                        } else if rs1 == 0 {
351                            let pvm_rd = self.require_reg(rd)?;
352                            let pvm_rs2 = self.require_reg(rs2)?;
353                            self.emit_inst(148); // CmovNzImm
354                            self.emit_data(pvm_rd | (pvm_rs2 << 4));
355                            self.emit_var_imm(0);
356                        } else {
357                            let pvm_rd = self.require_reg(rd)?;
358                            let pvm_rs1 = self.require_reg(rs1)?;
359                            let pvm_rs2 = self.require_reg(rs2)?;
360                            self.emit_inst(219); // CmovNz
361                            self.emit_data(pvm_rs1 | (pvm_rs2 << 4));
362                            self.emit_data(pvm_rd);
363                        }
364                    }
365                    _ => {
366                        return Err(TranspileError::UnsupportedInstruction {
367                            offset: _addr as usize,
368                            detail: format!("custom-0 funct7={:#x} funct3={}", funct7, funct3),
369                        });
370                    }
371                }
372            }
373            _ => {
374                return Err(TranspileError::UnsupportedInstruction {
375                    offset: _addr as usize,
376                    detail: format!("unknown opcode {:#x}", opcode),
377                });
378            }
379        }
380
381        Ok(())
382    }
383
384    fn translate_jalr(
385        &mut self,
386        rd: u8,
387        rs1: u8,
388        imm: i32,
389        addr: u64,
390    ) -> Result<(), TranspileError> {
391        // Check for auipc+jalr pair (PC-relative call/jump)
392        if let Some((auipc_rd, auipc_val)) = self.pending_auipc.take() {
393            if auipc_rd == rs1 {
394                // Combined auipc+jalr: target = auipc_val + imm
395                let target = (auipc_val as i64 + imm as i64) as u64;
396                if rd == 0 {
397                    // Tail call: just jump, no return address
398                    self.emit_jump(target);
399                } else {
400                    // Function call: fused load_imm_jump (opcode 80)
401                    let rv_return_addr = addr + 4;
402                    self.emit_call(rd, rv_return_addr, target)?;
403                }
404                return Ok(());
405            } else {
406                // auipc targeted a different register — emit it as load_imm
407                self.emit_load_imm(auipc_rd, auipc_val as i64)?;
408            }
409        }
410
411        // Plain JALR (no preceding auipc, or auipc was for different reg)
412        if rd == 0 {
413            // Tail call or return: jump_ind without saving return address.
414            // Handles ret (rs1=ra, imm=0) and tail calls through any register.
415            let pvm_rs1 = self.require_reg(rs1)?;
416            self.emit_inst(50); // jump_ind
417            self.emit_data(pvm_rs1);
418            self.emit_var_imm(imm);
419        } else {
420            // Indirect call (e.g. vtable dispatch): save return address then jump.
421            // Use load_imm_jump_ind (opcode 180): rd = return_addr, jump via rs1+imm.
422            let rv_return_addr = addr + 4;
423            let jt_idx = self.jump_table.len();
424            self.jump_table.push(0); // placeholder
425            self.return_fixups.push((jt_idx, rv_return_addr));
426            let jt_addr = ((jt_idx + 1) * 2) as i32;
427
428            let pvm_rd = self.require_reg(rd)?;
429            let pvm_rs1 = self.require_reg(rs1)?;
430            let lx = Self::var_imm_byte_count(jt_addr);
431
432            self.emit_inst(180); // load_imm_jump_ind
433            self.emit_data(pvm_rd | (pvm_rs1 << 4)); // reg_byte: ra=rd, rb=rs1
434            self.emit_data(lx as u8); // lx: byte count for imm_x (return addr)
435            self.emit_var_imm(jt_addr); // imm_x: jump table return address
436            self.emit_var_imm(imm); // imm_y: offset added to rs1
437        }
438        Ok(())
439    }
440
441    fn translate_branch(
442        &mut self,
443        funct3: u32,
444        rs1: u8,
445        rs2: u8,
446        target: u64,
447    ) -> Result<(), TranspileError> {
448        // Fuse load_imm + branch: if one operand was just loaded via load_imm,
449        // use the immediate branch form instead of a two-register branch.
450        // Saves one PVM instruction (the load_imm) per fused branch.
451        if let Some((load_rd, load_val, undo_pos)) = self.pending_load_imm.take()
452            && load_val >= i32::MIN as i64
453            && load_val <= i32::MAX as i64
454        {
455            let imm = load_val as i32;
456            // Check if rs2 is the loaded register: branch_*_imm rs1, imm, target
457            if rs2 == load_rd && rs1 != load_rd {
458                let pvm_rs1 = self.require_reg(rs1)?;
459                let pvm_opcode = match funct3 {
460                    0 => Some(81), // BEQ → branch_eq_imm
461                    1 => Some(82), // BNE → branch_ne_imm
462                    4 => Some(87), // BLT → branch_lt_s_imm
463                    5 => Some(89), // BGE → branch_ge_s_imm
464                    6 => Some(83), // BLTU → branch_lt_u_imm
465                    7 => Some(85), // BGEU → branch_ge_u_imm
466                    _ => None,
467                };
468                if let Some(opc) = pvm_opcode {
469                    self.code.truncate(undo_pos);
470                    self.bitmask.truncate(undo_pos);
471                    self.emit_branch_imm(opc, pvm_rs1, imm, target);
472                    return Ok(());
473                }
474            }
475            // Check if rs1 is the loaded register: flip the comparison
476            // BEQ/BNE are symmetric. BLT(rs1,rs2) with rs1=imm → BGE(rs2,imm+1) etc.
477            // Only handle symmetric cases (EQ, NE) to avoid off-by-one complexity.
478            if rs1 == load_rd && rs2 != load_rd {
479                let pvm_rs2 = self.require_reg(rs2)?;
480                let pvm_opcode = match funct3 {
481                    0 => Some(81), // BEQ is symmetric → branch_eq_imm rs2, imm
482                    1 => Some(82), // BNE is symmetric → branch_ne_imm rs2, imm
483                    _ => None,     // Inequalities need careful flipping, skip for now
484                };
485                if let Some(opc) = pvm_opcode {
486                    self.code.truncate(undo_pos);
487                    self.bitmask.truncate(undo_pos);
488                    self.emit_branch_imm(opc, pvm_rs2, imm, target);
489                    return Ok(());
490                }
491            }
492        }
493        // Couldn't fuse — load_imm was already emitted, just clear tracking
494
495        // When one operand is x0 (zero register), use immediate branch variants
496        // since PVM register 0 = RA, not zero.
497        if rs2 == 0 {
498            let pvm_rs1 = self.require_reg(rs1)?;
499            let pvm_opcode = match funct3 {
500                0 => 81, // BEQ x, x0 → branch_eq_imm x, 0
501                1 => 82, // BNE x, x0 → branch_ne_imm x, 0
502                4 => 87, // BLT x, x0 → branch_lt_s_imm x, 0
503                5 => 89, // BGE x, x0 → branch_ge_s_imm x, 0
504                6 => 83, // BLTU x, x0 → branch_lt_u_imm x, 0
505                7 => 85, // BGEU x, x0 → branch_ge_u_imm x, 0
506                _ => {
507                    return Err(TranspileError::UnsupportedInstruction {
508                        offset: 0,
509                        detail: format!("branch funct3={}", funct3),
510                    });
511                }
512            };
513            self.emit_branch_imm(pvm_opcode, pvm_rs1, 0, target);
514            return Ok(());
515        }
516
517        if rs1 == 0 {
518            // Compare x0 against rs2: flip the condition
519            let pvm_rs2 = self.require_reg(rs2)?;
520            match funct3 {
521                0 => self.emit_branch_imm(81, pvm_rs2, 0, target), // BEQ x0, y → branch_eq_imm y, 0
522                1 => self.emit_branch_imm(82, pvm_rs2, 0, target), // BNE x0, y → branch_ne_imm y, 0
523                4 => self.emit_branch_imm(89, pvm_rs2, 1, target), // BLT x0, rs2 → rs2 >= 1 (signed)
524                5 => self.emit_branch_imm(87, pvm_rs2, 1, target), // BGE x0, rs2 → rs2 < 1 (signed)
525                6 => self.emit_branch_imm(82, pvm_rs2, 0, target), // BLTU x0, rs2 → rs2 != 0
526                7 => self.emit_branch_imm(81, pvm_rs2, 0, target), // BGEU x0, rs2 → rs2 == 0
527                _ => {
528                    return Err(TranspileError::UnsupportedInstruction {
529                        offset: 0,
530                        detail: format!("branch funct3={}", funct3),
531                    });
532                }
533            };
534            return Ok(());
535        }
536
537        let pvm_rs1 = self.require_reg(rs1)?;
538        let pvm_rs2 = self.require_reg(rs2)?;
539
540        // Two register + one offset: opcodes 170-175
541        let pvm_opcode = match funct3 {
542            0 => 170, // BEQ → branch_eq
543            1 => 171, // BNE → branch_ne
544            4 => 173, // BLT → branch_lt_s
545            5 => 175, // BGE → branch_ge_s
546            6 => 172, // BLTU → branch_lt_u
547            7 => 174, // BGEU → branch_ge_u
548            _ => {
549                return Err(TranspileError::UnsupportedInstruction {
550                    offset: 0,
551                    detail: format!("branch funct3={}", funct3),
552                });
553            }
554        };
555
556        let inst_pc = self.code.len() as u32;
557        self.emit_inst(pvm_opcode);
558        self.emit_data(pvm_rs1 | (pvm_rs2 << 4));
559        // Fixup target offset (PC-relative)
560        let fixup_pos = self.code.len();
561        self.fixups.push((fixup_pos, target, 4));
562        self.fixup_pcs.insert(fixup_pos, inst_pc);
563        self.emit_imm32(0); // placeholder
564
565        Ok(())
566    }
567
568    pub(crate) fn translate_load(
569        &mut self,
570        funct3: u32,
571        rd: u8,
572        rs1: u8,
573        imm: i32,
574    ) -> Result<(), TranspileError> {
575        if rd == 0 {
576            return Ok(());
577        } // Write to x0 is a no-op
578
579        // Fuse load_imm + load_ind: if the base register was just loaded with
580        // a constant address, use the direct load form (OneRegOneImm) instead.
581        // Saves one PVM instruction per fused load.
582        if let Some((load_rd, load_val, undo_pos)) = self.pending_load_imm.take()
583            && rs1 == load_rd
584        {
585            let combined = load_val.wrapping_add(imm as i64);
586            if combined >= i32::MIN as i64 && combined <= i32::MAX as i64 {
587                let direct_opcode = match funct3 {
588                    0 => Some(53), // LB → load_i8
589                    1 => Some(55), // LH → load_i16
590                    2 => Some(57), // LW → load_i32
591                    3 => Some(58), // LD → load_u64
592                    4 => Some(52), // LBU → load_u8
593                    5 => Some(54), // LHU → load_u16
594                    6 => Some(56), // LWU → load_u32
595                    _ => None,
596                };
597                if let Some(opc) = direct_opcode {
598                    // Only truncate the load_imm if the load destination overwrites
599                    // the base register (rd == rs1). If rd != rs1, the base register
600                    // value may be needed later (e.g., RISC-V switch table pattern:
601                    // lw offset, table(base); add target, offset, base; jr target).
602                    if rd == rs1 {
603                        self.code.truncate(undo_pos);
604                        self.bitmask.truncate(undo_pos);
605                    }
606                    let pvm_rd = self.require_reg(rd)?;
607                    self.emit_inst(opc);
608                    self.emit_data(pvm_rd);
609                    self.emit_var_imm(combined as i32);
610                    return Ok(());
611                }
612            }
613        }
614        // Couldn't fuse — load_imm already emitted, just proceed
615
616        let pvm_rd = self.require_reg(rd)?;
617        let pvm_rs1 = self.require_reg(rs1)?;
618
619        // Two register + one immediate: load_ind_*
620        let pvm_opcode = match funct3 {
621            0 => 125, // LB → load_ind_i8
622            1 => 127, // LH → load_ind_i16
623            2 => 129, // LW → load_ind_i32
624            3 => 130, // LD → load_ind_u64
625            4 => 124, // LBU → load_ind_u8
626            5 => 126, // LHU → load_ind_u16
627            6 => 128, // LWU → load_ind_u32
628            _ => {
629                return Err(TranspileError::UnsupportedInstruction {
630                    offset: 0,
631                    detail: format!("load funct3={}", funct3),
632                });
633            }
634        };
635
636        self.emit_inst(pvm_opcode);
637        self.emit_data(pvm_rd | (pvm_rs1 << 4));
638        self.emit_var_imm(imm);
639
640        Ok(())
641    }
642
643    pub(crate) fn translate_store(
644        &mut self,
645        funct3: u32,
646        rs1: u8,
647        rs2: u8,
648        imm: i32,
649    ) -> Result<(), TranspileError> {
650        // Fuse load_imm + store: check if the base address or stored value was constant.
651        if let Some((load_rd, load_val, undo_pos)) = self.pending_load_imm.take()
652            && load_val >= i32::MIN as i64
653            && load_val <= i32::MAX as i64
654        {
655            // Case 1: Base register was loaded with constant address → direct store.
656            // store_ind_* data, base, offset  where base = constant addr
657            //   → store_* data, (addr + offset)
658            if rs1 == load_rd && rs2 != load_rd && rs2 != 0 {
659                let combined = load_val.wrapping_add(imm as i64);
660                if combined >= i32::MIN as i64 && combined <= i32::MAX as i64 {
661                    let direct_opcode = match funct3 {
662                        0 => Some(59), // SB → store_u8
663                        1 => Some(60), // SH → store_u16
664                        2 => Some(61), // SW → store_u32
665                        3 => Some(62), // SD → store_u64
666                        _ => None,
667                    };
668                    if let Some(opc) = direct_opcode {
669                        self.code.truncate(undo_pos);
670                        self.bitmask.truncate(undo_pos);
671                        let pvm_rs2 = self.require_reg(rs2)?;
672                        self.emit_inst(opc);
673                        self.emit_data(pvm_rs2);
674                        self.emit_var_imm(combined as i32);
675                        return Ok(());
676                    }
677                }
678            }
679            // Case 2: Value register was loaded with constant → store_imm_ind.
680            // NOTE: We intentionally do NOT undo the load_imm here because
681            // the register may still be needed after the store (e.g., as a
682            // function argument). The load_imm was already emitted, so the
683            // register holds the correct value. Just emit a normal store.
684            // The load_imm + store costs two instructions instead of one
685            // fused store_imm_ind, but is always correct.
686        }
687        // Couldn't fuse — load_imm was already emitted, just clear tracking
688
689        // x0 (zero register) has no PVM equivalent — PVM reg 0 is RA, not zero.
690        // Use store_imm_ind_* to store a literal zero instead.
691        if rs2 == 0 {
692            let pvm_rs1 = self.require_reg(rs1)?;
693            let pvm_opcode = match funct3 {
694                0 => 70, // store_imm_ind_u8
695                1 => 71, // store_imm_ind_u16
696                2 => 72, // store_imm_ind_u32
697                3 => 73, // store_imm_ind_u64
698                _ => {
699                    return Err(TranspileError::UnsupportedInstruction {
700                        offset: 0,
701                        detail: format!("store funct3={}", funct3),
702                    });
703                }
704            };
705            // Format: OneRegTwoImm — reg_byte encodes ra + imm_x length
706            // reg_byte = ra | (lx << 4)
707            // imm_y has length 0, which decodes as 0 (the value we want to store)
708            let (lx, imm_bytes) = encode_var_imm(imm);
709            self.emit_inst(pvm_opcode);
710            self.emit_data(pvm_rs1 | (lx << 4));
711            for b in &imm_bytes {
712                self.emit_data(*b);
713            }
714            return Ok(());
715        }
716
717        let pvm_rs2 = self.require_reg(rs2)?; // data register → rD
718        let pvm_rs1 = self.require_reg(rs1)?; // base register → rA
719
720        let pvm_opcode = match funct3 {
721            0 => 120, // SB → store_ind_u8
722            1 => 121, // SH → store_ind_u16
723            2 => 122, // SW → store_ind_u32
724            3 => 123, // SD → store_ind_u64
725            _ => {
726                return Err(TranspileError::UnsupportedInstruction {
727                    offset: 0,
728                    detail: format!("store funct3={}", funct3),
729                });
730            }
731        };
732
733        self.emit_inst(pvm_opcode);
734        self.emit_data(pvm_rs2 | (pvm_rs1 << 4));
735        self.emit_var_imm(imm);
736
737        Ok(())
738    }
739
740    fn translate_op_imm(
741        &mut self,
742        funct3: u32,
743        funct7: u32,
744        rd: u8,
745        rs1: u8,
746        imm: i32,
747    ) -> Result<(), TranspileError> {
748        // Track `li t0, N` (ADDI x5, x0, N) for ecall ID translation
749        if funct3 == 0 && rd == 5 && rs1 == 0 {
750            self.last_t0_imm = Some(imm);
751        }
752
753        // AUIPC+ADDI fusion: compute full address. If it's a code address (function
754        // pointer), emit a jump table entry so indirect calls via djump work.
755        if let Some((auipc_rd, auipc_val)) = self.pending_auipc.take() {
756            if funct3 == 0 && rd == auipc_rd && rs1 == auipc_rd && rd != 0 {
757                let full_addr = (auipc_val as i64 + imm as i64) as u64;
758                if self.is_code_addr(full_addr) {
759                    let jt_idx = self.jump_table.len();
760                    self.jump_table.push(0); // placeholder, resolved by apply_fixups
761                    self.return_fixups.push((jt_idx, full_addr));
762                    let jt_addr = ((jt_idx + 1) * 2) as i64;
763                    let pos = self.code.len();
764                    self.emit_load_imm(rd, jt_addr)?;
765                    self.pending_load_imm = Some((rd, jt_addr, pos));
766                } else {
767                    let combined = auipc_val as i64 + imm as i64;
768                    let pos = self.code.len();
769                    self.emit_load_imm(rd, combined)?;
770                    self.pending_load_imm = Some((rd, combined, pos));
771                }
772                return Ok(());
773            }
774            // Not a matching ADDI — flush the pending AUIPC
775            self.emit_load_imm(auipc_rd, auipc_val as i64)?;
776        }
777
778        // LUI+ADDI fusion: if there's a pending LUI and this is ADDI rd, rd, imm
779        // with the same register, fuse into a single load_imm with the combined value.
780        if let Some((lui_rd, lui_val)) = self.pending_lui.take() {
781            if funct3 == 0 && rd == lui_rd && rs1 == lui_rd && rd != 0 {
782                let combined = lui_val.wrapping_add(imm as i64);
783                // Track for potential fusion with subsequent ALU op
784                let pos = self.code.len();
785                self.emit_load_imm(rd, combined)?;
786                self.pending_load_imm = Some((rd, combined, pos));
787                return Ok(());
788            }
789            // Not a matching ADDI — flush the pending LUI
790            self.emit_load_imm(lui_rd, lui_val)?;
791        }
792
793        if rd == 0 {
794            return Ok(());
795        } // Write to x0 is a no-op in RISC-V
796
797        // ADDI rd, rs, 0 is the RISC-V `mv rd, rs` pseudo-instruction.
798        // Use compact move_reg (2 bytes) instead of add_imm (6 bytes).
799        if funct3 == 0 && imm == 0 && rs1 != 0 {
800            if rd == rs1 {
801                // ADDI rd, rd, 0 is a NOP
802                self.emit_inst(1); // fallthrough
803                return Ok(());
804            }
805            let pvm_rd = self.require_reg(rd)?;
806            let pvm_rs1 = self.require_reg(rs1)?;
807            self.emit_inst(100); // move_reg
808            self.emit_data(pvm_rd | (pvm_rs1 << 4));
809            return Ok(());
810        }
811
812        // When rs1 = x0 (zero register), treat as loading immediate directly
813        // because PVM has no zero register — x0 maps to RA which is NOT zero.
814        if rs1 == 0 {
815            match funct3 {
816                0 => return self.emit_load_imm(rd, imm as i64), // li rd, imm
817                2 => {
818                    // SLTI rd, x0, imm → rd = (0 < imm) ? 1 : 0
819                    return self.emit_load_imm(rd, if 0 < imm { 1 } else { 0 });
820                }
821                3 => {
822                    // SLTIU rd, x0, imm → rd = (0 < imm unsigned) ? 1 : 0
823                    return self.emit_load_imm(rd, if imm != 0 { 1 } else { 0 });
824                }
825                4 => return self.emit_load_imm(rd, imm as i64), // XORI rd, x0, imm = imm
826                6 => return self.emit_load_imm(rd, imm as i64), // ORI rd, x0, imm = imm
827                7 => return self.emit_load_imm(rd, 0),          // ANDI rd, x0, imm = 0
828                _ => {}                                         // shifts with x0 → just 0, but rare
829            }
830        }
831
832        let pvm_rd = self.require_reg(rd)?;
833        let pvm_rs1 = self.require_reg(rs1)?;
834
835        // RV32 uses 32-bit PVM ops; RV64 uses 64-bit PVM ops
836        let pvm_opcode = match funct3 {
837            0 => {
838                if self.is_64bit {
839                    149
840                } else {
841                    131
842                }
843            } // ADDI → add_imm_64/32
844            1 => {
845                if funct7 == 0x30 {
846                    // Zbb unary: clz/ctz/cpop/sext.b/sext.h
847                    let rs2 = (imm & 0x1F) as u8;
848                    let opc = match rs2 {
849                        0 => {
850                            if self.is_64bit {
851                                104
852                            } else {
853                                105
854                            }
855                        }
856                        1 => {
857                            if self.is_64bit {
858                                106
859                            } else {
860                                107
861                            }
862                        }
863                        2 => {
864                            if self.is_64bit {
865                                102
866                            } else {
867                                103
868                            }
869                        }
870                        4 => 108,
871                        5 => 109,
872                        _ => {
873                            return Err(TranspileError::UnsupportedInstruction {
874                                offset: 0,
875                                detail: format!("Zbb rs2={}", rs2),
876                            });
877                        }
878                    };
879                    self.emit_inst(opc);
880                    self.emit_data(pvm_rd | (pvm_rs1 << 4));
881                    return Ok(());
882                }
883                // SLLI (funct7=0x00)
884                let shamt = imm & if self.is_64bit { 0x3F } else { 0x1F };
885                self.emit_inst(if self.is_64bit { 151 } else { 138 });
886                self.emit_data(pvm_rd | (pvm_rs1 << 4));
887                self.emit_var_imm(shamt);
888                return Ok(());
889            }
890            2 => 137, // SLTI → set_lt_s_imm
891            3 => 136, // SLTIU → set_lt_u_imm
892            4 => 133, // XORI → xor_imm
893            5 => {
894                if (funct7 == 0x35 || funct7 == 0x34) && (imm & 0x1F) == 0x18 {
895                    // Zbb rev8 (RV64: 0x35, RV32: 0x34)
896                    self.emit_inst(111);
897                    self.emit_data(pvm_rd | (pvm_rs1 << 4));
898                    return Ok(());
899                }
900                if funct7 == 0x30 || funct7 == 0x31 {
901                    // Zbb RORI: funct6=0x18 (bits 31:26).
902                    // funct7=0x30 when shamt<32, funct7=0x31 when shamt>=32 (bit 25 set).
903                    let shamt = imm & if self.is_64bit { 0x3F } else { 0x1F };
904                    self.emit_inst(if self.is_64bit { 158 } else { 160 });
905                    self.emit_data(pvm_rd | (pvm_rs1 << 4));
906                    self.emit_var_imm(shamt);
907                    return Ok(());
908                }
909                if funct7 == 0x14 {
910                    // Zbb orc.b — OR-combine bytes
911                    // No PVM equivalent; emit as trap (rare instruction)
912                    return Err(TranspileError::UnsupportedInstruction {
913                        offset: 0,
914                        detail: "Zbb orc.b not yet supported".into(),
915                    });
916                }
917                // SRLI (funct7=0x00) / SRAI (funct7=0x20+)
918                let shamt = imm & if self.is_64bit { 0x3F } else { 0x1F };
919                if funct7 & 0x20 != 0 {
920                    self.emit_inst(if self.is_64bit { 153 } else { 140 }); // shar_r_imm
921                } else {
922                    self.emit_inst(if self.is_64bit { 152 } else { 139 }); // shlo_r_imm
923                }
924                self.emit_data(pvm_rd | (pvm_rs1 << 4));
925                self.emit_var_imm(shamt);
926                return Ok(());
927            }
928            6 => 134, // ORI → or_imm
929            7 => 132, // ANDI → and_imm
930            _ => unreachable!(),
931        };
932
933        self.emit_inst(pvm_opcode);
934        self.emit_data(pvm_rd | (pvm_rs1 << 4));
935        self.emit_var_imm(imm);
936
937        Ok(())
938    }
939
940    fn translate_op(
941        &mut self,
942        funct3: u32,
943        funct7: u32,
944        rd: u8,
945        rs1: u8,
946        rs2: u8,
947        addr: u64,
948    ) -> Result<(), TranspileError> {
949        if rd == 0 {
950            return Ok(());
951        } // Write to x0 is a no-op in RISC-V
952
953        // Fuse load_imm + ALU op: if one operand was just loaded via load_imm
954        // and the value fits in i32, undo the load_imm and emit the immediate
955        // form instead (saves one instruction).
956        if let Some((load_rd, load_val, undo_pos)) = self.pending_load_imm.take()
957            && load_val >= i32::MIN as i64
958            && load_val <= i32::MAX as i64
959        {
960            let imm = load_val as i32;
961            // Check if rs2 is the loaded register (ADD/AND/OR/XOR rd, rs1, load_rd)
962            let (fuse_base, _commutative) = if rs2 == load_rd && rs1 != load_rd {
963                (Some(rs1), true)
964            } else if rs1 == load_rd && rs2 != load_rd && (funct7, funct3) != (0x20, 0) {
965                // rs1 is the loaded register — only for commutative ops (not SUB)
966                (Some(rs2), true)
967            } else {
968                (None, false)
969            };
970
971            if let Some(base) = fuse_base {
972                let pvm_imm_opcode = match (funct7, funct3) {
973                    (0, 0) => Some(if self.is_64bit { 149 } else { 131 }), // ADD → add_imm
974                    (0, 7) => Some(132),                                   // AND → and_imm
975                    (0, 6) => Some(134),                                   // OR → or_imm
976                    (0, 4) => Some(133),                                   // XOR → xor_imm
977                    (1, 0) => Some(if self.is_64bit { 150 } else { 135 }), // MUL → mul_imm
978                    (0, 2) => Some(137),                                   // SLT → set_lt_s_imm
979                    (0, 3) => Some(136),                                   // SLTU → set_lt_u_imm
980                    _ => None,
981                };
982
983                if let Some(pvm_opcode) = pvm_imm_opcode {
984                    // Only truncate the load_imm if the loaded register IS the
985                    // destination (rd == load_rd). If it's just an operand,
986                    // keep the load_imm so the register retains its value for
987                    // future use (e.g., switch table: add idx, base, idx; lw off, 0(idx);
988                    // add target, off, base; jr target — base is used twice).
989                    if rd == load_rd {
990                        self.code.truncate(undo_pos);
991                        self.bitmask.truncate(undo_pos);
992                        self.address_map.insert(addr, undo_pos as u32);
993                    }
994                    let pvm_rd = self.require_reg(rd)?;
995                    let pvm_base = self.require_reg(base)?;
996                    self.emit_inst(pvm_opcode);
997                    self.emit_data(pvm_rd | (pvm_base << 4));
998                    self.emit_var_imm(imm);
999                    return Ok(());
1000                }
1001            }
1002
1003            // Special case: shifts with loaded shift count → shift immediate forms.
1004            // SLL/SRL/SRA rd, rs1, load_rd → shlo_l/shlo_r/shar_r_imm rd, rs1, imm
1005            if rs2 == load_rd && matches!((funct7, funct3), (0, 1) | (0, 5) | (0x20, 5)) {
1006                let pvm_imm_opcode = match (funct7, funct3) {
1007                    (0, 1) => {
1008                        if self.is_64bit {
1009                            151
1010                        } else {
1011                            138
1012                        }
1013                    } // SLL → shlo_l_imm
1014                    (0, 5) => {
1015                        if self.is_64bit {
1016                            152
1017                        } else {
1018                            139
1019                        }
1020                    } // SRL → shlo_r_imm
1021                    (0x20, 5) => {
1022                        if self.is_64bit {
1023                            153
1024                        } else {
1025                            140
1026                        }
1027                    } // SRA → shar_r_imm
1028                    _ => unreachable!(),
1029                };
1030                self.code.truncate(undo_pos);
1031                self.bitmask.truncate(undo_pos);
1032                self.address_map.insert(addr, undo_pos as u32);
1033                let pvm_rd = self.require_reg(rd)?;
1034                let pvm_rs1 = self.require_reg(rs1)?;
1035                self.emit_inst(pvm_imm_opcode);
1036                self.emit_data(pvm_rd | (pvm_rs1 << 4));
1037                self.emit_var_imm(imm);
1038                return Ok(());
1039            }
1040
1041            // Special case: SUB rd, rs1, load_rd → neg_add_imm rd, rs1, -imm
1042            // SUB is not commutative, but if rs2 is the loaded register:
1043            // rd = rs1 - imm = rs1 + (-imm)
1044            if (funct7, funct3) == (0x20, 0) && rs2 == load_rd && rs1 != load_rd {
1045                let neg_imm = (-(load_val as i32) as i64) as i32;
1046                // Use add_imm with negated immediate (avoids neg_add_imm)
1047                let pvm_opcode = if self.is_64bit { 149 } else { 131 }; // add_imm
1048                self.code.truncate(undo_pos);
1049                self.bitmask.truncate(undo_pos);
1050                self.address_map.insert(addr, undo_pos as u32);
1051                let pvm_rd = self.require_reg(rd)?;
1052                let pvm_rs1 = self.require_reg(rs1)?;
1053                self.emit_inst(pvm_opcode);
1054                self.emit_data(pvm_rd | (pvm_rs1 << 4));
1055                self.emit_var_imm(neg_imm);
1056                return Ok(());
1057            }
1058        }
1059        // Couldn't fuse — load_imm is already emitted, just proceed normally
1060
1061        // Handle x0 as source: PVM reg 0 = RA, not zero.
1062        if rs1 == 0 && funct7 == 0 && funct3 == 0 {
1063            // add rd, x0, rs2 → mv rd, rs2
1064            let pvm_rd = self.require_reg(rd)?;
1065            let pvm_rs2 = self.require_reg(rs2)?;
1066            self.emit_inst(100); // move_reg
1067            self.emit_data(pvm_rd | (pvm_rs2 << 4));
1068            return Ok(());
1069        }
1070        if rs2 == 0 && funct7 == 0 && funct3 == 0 {
1071            // add rd, rs1, x0 → mv rd, rs1
1072            let pvm_rd = self.require_reg(rd)?;
1073            let pvm_rs1 = self.require_reg(rs1)?;
1074            self.emit_inst(100); // move_reg
1075            self.emit_data(pvm_rd | (pvm_rs1 << 4));
1076            return Ok(());
1077        }
1078        // SUB rd, x0, rs2 → neg rd, rs2
1079        if rs1 == 0 && funct7 == 0x20 && funct3 == 0 {
1080            let pvm_rd = self.require_reg(rd)?;
1081            let pvm_rs2 = self.require_reg(rs2)?;
1082            let neg_op = if self.is_64bit { 154 } else { 141 }; // neg_add_imm_64/32
1083            self.emit_inst(neg_op);
1084            self.emit_data(pvm_rd | (pvm_rs2 << 4));
1085            self.emit_var_imm(0);
1086            return Ok(());
1087        }
1088        // Handle remaining x0 source cases
1089        if rs1 == 0 {
1090            let pvm_rd = self.require_reg(rd)?;
1091            let pvm_rs2 = self.require_reg(rs2)?;
1092            match (funct7, funct3) {
1093                (0, 1) | (0, 5) | (0x20, 5) => {
1094                    // SLL/SRL/SRA rd, x0, rs2 → shift 0 by rs2 = 0
1095                    return self.emit_load_imm(rd, 0);
1096                }
1097                (0, 4) | (0, 6) => {
1098                    // XOR/OR rd, x0, rs2 → rs2
1099                    self.emit_inst(100); // move_reg
1100                    self.emit_data(pvm_rd | (pvm_rs2 << 4));
1101                    return Ok(());
1102                }
1103                (0, 7) => {
1104                    // AND rd, x0, rs2 → 0
1105                    return self.emit_load_imm(rd, 0);
1106                }
1107                (0, 3) => {
1108                    // SLTU rd, x0, rs2 → snez rd, rs2
1109                    // When rd == rs2, skip the load_imm to avoid clobbering
1110                    // the value before the conditional check.
1111                    if pvm_rd != pvm_rs2 {
1112                        self.emit_load_imm(rd, 0)?;
1113                    }
1114                    self.emit_inst(148); // cmov_nz_imm: if rs2 != 0 then rd = imm
1115                    self.emit_data(pvm_rd | (pvm_rs2 << 4));
1116                    self.emit_var_imm(1);
1117                    return Ok(());
1118                }
1119                (1, _) => {
1120                    // M extension with x0 → result is 0
1121                    return self.emit_load_imm(rd, 0);
1122                }
1123                _ => {
1124                    return Err(TranspileError::UnsupportedInstruction {
1125                        offset: addr as usize,
1126                        detail: format!(
1127                            "unhandled x0-as-rs1 op: funct7={funct7:#x} funct3={funct3}"
1128                        ),
1129                    });
1130                }
1131            }
1132        }
1133        if rs2 == 0 {
1134            let pvm_rd = self.require_reg(rd)?;
1135            let pvm_rs1 = self.require_reg(rs1)?;
1136            match (funct7, funct3) {
1137                (0, 2) | (0, 3) => {
1138                    // slt(u) rd, rs1, x0 → set_lt_(s|u)_imm rd, rs1, 0
1139                    let pvm_opcode = if funct3 == 2 { 137 } else { 136 };
1140                    self.emit_inst(pvm_opcode);
1141                    self.emit_data(pvm_rd | (pvm_rs1 << 4));
1142                    self.emit_var_imm(0);
1143                    return Ok(());
1144                }
1145                (0x20, 0) | (0, 4) | (0, 6) => {
1146                    // SUB/XOR/OR rd, rs1, x0 → rs1 op 0 = rs1 → move
1147                    self.emit_inst(100); // move_reg
1148                    self.emit_data(pvm_rd | (pvm_rs1 << 4));
1149                    return Ok(());
1150                }
1151                (0, 7) => {
1152                    // AND rd, rs1, x0 → 0
1153                    return self.emit_load_imm(rd, 0);
1154                }
1155                (0, 1) | (0, 5) | (0x20, 5) => {
1156                    // SLL/SRL/SRA rd, rs1, x0 → shift by 0 = rs1 → move
1157                    self.emit_inst(100); // move_reg
1158                    self.emit_data(pvm_rd | (pvm_rs1 << 4));
1159                    return Ok(());
1160                }
1161                (1, _) => {
1162                    // M extension: mul rd, rs1, 0 = 0; div/rem by 0 is undefined
1163                    return self.emit_load_imm(rd, 0);
1164                }
1165                _ => {
1166                    return Err(TranspileError::UnsupportedInstruction {
1167                        offset: addr as usize,
1168                        detail: format!(
1169                            "unhandled x0-as-rs2 op: funct7={funct7:#x} funct3={funct3}"
1170                        ),
1171                    });
1172                }
1173            }
1174        }
1175
1176        let pvm_rd = self.require_reg(rd)?;
1177        let pvm_rs1 = self.require_reg(rs1)?;
1178        let pvm_rs2 = self.require_reg(rs2)?;
1179
1180        // RV32 uses 32-bit PVM ops; RV64 uses 64-bit PVM ops
1181        let pvm_opcode = if funct7 == 1 {
1182            // M extension (multiply/divide)
1183            match funct3 {
1184                0 => {
1185                    if self.is_64bit {
1186                        202
1187                    } else {
1188                        192
1189                    }
1190                } // MUL
1191                1 => 213, // MULH → mul_upper_ss (always 64-bit, gives upper bits)
1192                2 => 215, // MULHSU → mul_upper_su
1193                3 => 214, // MULHU → mul_upper_uu
1194                4 => {
1195                    if self.is_64bit {
1196                        204
1197                    } else {
1198                        194
1199                    }
1200                } // DIV
1201                5 => {
1202                    if self.is_64bit {
1203                        203
1204                    } else {
1205                        193
1206                    }
1207                } // DIVU
1208                6 => {
1209                    if self.is_64bit {
1210                        206
1211                    } else {
1212                        196
1213                    }
1214                } // REM
1215                7 => {
1216                    if self.is_64bit {
1217                        205
1218                    } else {
1219                        195
1220                    }
1221                } // REMU
1222                _ => unreachable!(),
1223            }
1224        } else if funct7 == 0x20 {
1225            match funct3 {
1226                0 => {
1227                    if self.is_64bit {
1228                        201
1229                    } else {
1230                        191
1231                    }
1232                } // SUB
1233                5 => {
1234                    if self.is_64bit {
1235                        209
1236                    } else {
1237                        199
1238                    }
1239                } // SRA
1240                7 | 6 | 4 => {
1241                    // Zbb ANDN/ORN/XNOR: rd = rs1 OP ~rs2
1242                    if rd == rs1 && rd == rs2 {
1243                        return self.emit_load_imm(rd, if funct3 == 7 { 0 } else { -1i64 });
1244                    }
1245                    let alu: u8 = match funct3 {
1246                        7 => 210,
1247                        6 => 212,
1248                        _ => 211,
1249                    };
1250                    if rd != rs1 {
1251                        // use rd as temp for ~rs2
1252                        self.emit_inst(133);
1253                        self.emit_data(pvm_rd | (pvm_rs2 << 4));
1254                        self.emit_var_imm(-1);
1255                        self.emit_inst(alu);
1256                        self.emit_data(pvm_rs1 | (pvm_rd << 4));
1257                        self.emit_data(pvm_rd);
1258                    } else {
1259                        // rd==rs1: NOT rs2 in-place, OP, restore
1260                        self.emit_inst(133);
1261                        self.emit_data(pvm_rs2 | (pvm_rs2 << 4));
1262                        self.emit_var_imm(-1);
1263                        self.emit_inst(alu);
1264                        self.emit_data(pvm_rd | (pvm_rs2 << 4));
1265                        self.emit_data(pvm_rd);
1266                        self.emit_inst(133);
1267                        self.emit_data(pvm_rs2 | (pvm_rs2 << 4));
1268                        self.emit_var_imm(-1);
1269                    }
1270                    return Ok(());
1271                }
1272                _ => {
1273                    return Err(TranspileError::UnsupportedInstruction {
1274                        offset: 0,
1275                        detail: format!("OP funct7=0x20 funct3={}", funct3),
1276                    });
1277                }
1278            }
1279        } else if funct7 == 0x05 {
1280            // Zbb min/max
1281            match funct3 {
1282                4 => 229,
1283                5 => 230,
1284                6 => 227,
1285                7 => 228,
1286                _ => {
1287                    return Err(TranspileError::UnsupportedInstruction {
1288                        offset: 0,
1289                        detail: format!("Zbb f7=5 f3={}", funct3),
1290                    });
1291                }
1292            }
1293        } else if funct7 == 0x30 {
1294            // Zbb rotations — emit and return early
1295            let opc = match funct3 {
1296                1 => {
1297                    if self.is_64bit {
1298                        220
1299                    } else {
1300                        221
1301                    }
1302                }
1303                5 => {
1304                    if self.is_64bit {
1305                        222
1306                    } else {
1307                        223
1308                    }
1309                }
1310                _ => {
1311                    return Err(TranspileError::UnsupportedInstruction {
1312                        offset: 0,
1313                        detail: format!("Zbb f7=30 f3={}", funct3),
1314                    });
1315                }
1316            };
1317            self.emit_inst(opc);
1318            self.emit_data(pvm_rs1 | (pvm_rs2 << 4));
1319            self.emit_data(pvm_rd);
1320            return Ok(());
1321        } else if funct7 == 0 {
1322            match funct3 {
1323                0 => {
1324                    if self.is_64bit {
1325                        200
1326                    } else {
1327                        190
1328                    }
1329                } // ADD
1330                1 => {
1331                    if self.is_64bit {
1332                        207
1333                    } else {
1334                        197
1335                    }
1336                } // SLL
1337                2 => 217,
1338                3 => 216,
1339                4 => 211,
1340                5 => {
1341                    if self.is_64bit {
1342                        208
1343                    } else {
1344                        198
1345                    }
1346                }
1347                6 => 212,
1348                7 => 210,
1349                _ => unreachable!(),
1350            }
1351        } else {
1352            return Err(TranspileError::UnsupportedInstruction {
1353                offset: 0,
1354                detail: format!("OP funct7={:#x} funct3={}", funct7, funct3),
1355            });
1356        };
1357
1358        // ThreeReg encoding: byte1 = rA | (rB << 4), byte2 = rD
1359        self.emit_inst(pvm_opcode);
1360        self.emit_data(pvm_rs1 | (pvm_rs2 << 4));
1361        self.emit_data(pvm_rd);
1362
1363        Ok(())
1364    }
1365
1366    fn translate_op_imm_32(
1367        &mut self,
1368        funct3: u32,
1369        funct7: u32,
1370        rd: u8,
1371        rs1: u8,
1372        imm: i32,
1373    ) -> Result<(), TranspileError> {
1374        if rd == 0 {
1375            return Ok(());
1376        }
1377        let pvm_rd = self.require_reg(rd)?;
1378        let pvm_rs1 = self.require_reg(rs1)?;
1379
1380        match funct3 {
1381            0 => {
1382                // ADDIW → add_imm_32
1383                self.emit_inst(131);
1384                self.emit_data(pvm_rd | (pvm_rs1 << 4));
1385                self.emit_var_imm(imm);
1386            }
1387            1 => {
1388                if funct7 == 0x30 {
1389                    // Zbb: clzw(rs2=0), ctzw(rs2=1), cpopw(rs2=2)
1390                    let rs2 = (imm & 0x1F) as u8;
1391                    let opc = match rs2 {
1392                        0 => 105,
1393                        1 => 107,
1394                        2 => 103,
1395                        _ => {
1396                            return Err(TranspileError::UnsupportedInstruction {
1397                                offset: 0,
1398                                detail: format!("Zbb-W rs2={}", rs2),
1399                            });
1400                        }
1401                    };
1402                    self.emit_inst(opc);
1403                    self.emit_data(pvm_rd | (pvm_rs1 << 4));
1404                } else {
1405                    // SLLIW (funct7=0x00)
1406                    let shamt = imm & 0x1F;
1407                    self.emit_inst(138);
1408                    self.emit_data(pvm_rd | (pvm_rs1 << 4));
1409                    self.emit_var_imm(shamt);
1410                }
1411            }
1412            5 => {
1413                if funct7 == 0x30 {
1414                    // Zbb roriw
1415                    let shamt = imm & 0x1F;
1416                    self.emit_inst(160);
1417                    self.emit_data(pvm_rd | (pvm_rs1 << 4));
1418                    self.emit_var_imm(shamt);
1419                } else if funct7 & 0x20 != 0 {
1420                    // SRAIW (funct7=0x20)
1421                    let shamt = imm & 0x1F;
1422                    self.emit_inst(140);
1423                    self.emit_data(pvm_rd | (pvm_rs1 << 4));
1424                    self.emit_var_imm(shamt);
1425                } else {
1426                    // SRLIW (funct7=0x00)
1427                    let shamt = imm & 0x1F;
1428                    self.emit_inst(139);
1429                    self.emit_data(pvm_rd | (pvm_rs1 << 4));
1430                    self.emit_var_imm(shamt);
1431                }
1432            }
1433            _ => {
1434                return Err(TranspileError::UnsupportedInstruction {
1435                    offset: 0,
1436                    detail: format!("OP-IMM-32 funct3={}", funct3),
1437                });
1438            }
1439        }
1440
1441        Ok(())
1442    }
1443
1444    fn translate_op_32(
1445        &mut self,
1446        funct3: u32,
1447        funct7: u32,
1448        rd: u8,
1449        rs1: u8,
1450        rs2: u8,
1451    ) -> Result<(), TranspileError> {
1452        if rd == 0 {
1453            return Ok(());
1454        }
1455
1456        // Fuse load_imm + 32-bit ALU op: ADDW, MULW, SUB as negated ADD.
1457        if let Some((load_rd, load_val, undo_pos)) = self.pending_load_imm.take()
1458            && load_val >= i32::MIN as i64
1459            && load_val <= i32::MAX as i64
1460        {
1461            let imm = load_val as i32;
1462            let (fuse_base, _comm) = if rs2 == load_rd && rs1 != load_rd {
1463                (Some(rs1), true)
1464            } else if rs1 == load_rd && rs2 != load_rd && (funct7, funct3) != (0x20, 0) {
1465                (Some(rs2), true)
1466            } else {
1467                (None, false)
1468            };
1469
1470            if let Some(base) = fuse_base {
1471                let pvm_imm_opcode = match (funct7, funct3) {
1472                    (0, 0) => Some(131), // ADDW → add_imm_32
1473                    (1, 0) => Some(135), // MULW → mul_imm_32
1474                    _ => None,
1475                };
1476                if let Some(pvm_opcode) = pvm_imm_opcode {
1477                    self.code.truncate(undo_pos);
1478                    self.bitmask.truncate(undo_pos);
1479                    let pvm_rd = self.require_reg(rd)?;
1480                    let pvm_base = self.require_reg(base)?;
1481                    self.emit_inst(pvm_opcode);
1482                    self.emit_data(pvm_rd | (pvm_base << 4));
1483                    self.emit_var_imm(imm);
1484                    return Ok(());
1485                }
1486            }
1487            // 32-bit shifts with loaded shift count → shift immediate forms.
1488            // SLLW/SRLW/SRAW rd, rs1, load_rd → shlo_l/shlo_r/shar_r_imm_32
1489            if rs2 == load_rd && matches!((funct7, funct3), (0, 1) | (0, 5) | (0x20, 5)) {
1490                let pvm_imm_opcode = match (funct7, funct3) {
1491                    (0, 1) => 138,    // SLLW → shlo_l_imm_32
1492                    (0, 5) => 139,    // SRLW → shlo_r_imm_32
1493                    (0x20, 5) => 140, // SRAW → shar_r_imm_32
1494                    _ => unreachable!(),
1495                };
1496                self.code.truncate(undo_pos);
1497                self.bitmask.truncate(undo_pos);
1498                let pvm_rd = self.require_reg(rd)?;
1499                let pvm_rs1 = self.require_reg(rs1)?;
1500                self.emit_inst(pvm_imm_opcode);
1501                self.emit_data(pvm_rd | (pvm_rs1 << 4));
1502                self.emit_var_imm(imm);
1503                return Ok(());
1504            }
1505
1506            // SUB with loaded rs2: SUBW rd, rs1, load_rd → add_imm_32 rd, rs1, -imm
1507            if (funct7, funct3) == (0x20, 0) && rs2 == load_rd && rs1 != load_rd {
1508                let neg_imm = (-(load_val as i32) as i64) as i32;
1509                self.code.truncate(undo_pos);
1510                self.bitmask.truncate(undo_pos);
1511                let pvm_rd = self.require_reg(rd)?;
1512                let pvm_rs1 = self.require_reg(rs1)?;
1513                self.emit_inst(131); // add_imm_32
1514                self.emit_data(pvm_rd | (pvm_rs1 << 4));
1515                self.emit_var_imm(neg_imm);
1516                return Ok(());
1517            }
1518        }
1519        // Couldn't fuse
1520
1521        // Handle x0 as source: PVM reg 0 = RA, not zero.
1522        if rs1 == 0 {
1523            let pvm_rd = self.require_reg(rd)?;
1524            let pvm_rs2 = self.require_reg(rs2)?;
1525            match (funct7, funct3) {
1526                (0, 0) => {
1527                    // ADDW rd, x0, rs2 → sext.w rd, rs2 (sign-extend lower 32 bits)
1528                    self.emit_inst(131); // add_imm_32
1529                    self.emit_data(pvm_rd | (pvm_rs2 << 4));
1530                    self.emit_var_imm(0);
1531                    return Ok(());
1532                }
1533                (0x20, 0) => {
1534                    // SUBW rd, x0, rs2 → negw rd, rs2
1535                    self.emit_inst(141); // neg_add_imm_32
1536                    self.emit_data(pvm_rd | (pvm_rs2 << 4));
1537                    self.emit_var_imm(0);
1538                    return Ok(());
1539                }
1540                _ => {
1541                    return Err(TranspileError::UnsupportedInstruction {
1542                        offset: 0,
1543                        detail: format!("OP-32 x0-as-rs1: funct7={:#x} funct3={}", funct7, funct3),
1544                    });
1545                }
1546            }
1547        }
1548        if rs2 == 0 {
1549            let pvm_rd = self.require_reg(rd)?;
1550            let pvm_rs1 = self.require_reg(rs1)?;
1551            match (funct7, funct3) {
1552                (0, 0) => {
1553                    // ADDW rd, rs1, x0 → sext.w rd, rs1
1554                    self.emit_inst(131); // add_imm_32
1555                    self.emit_data(pvm_rd | (pvm_rs1 << 4));
1556                    self.emit_var_imm(0);
1557                    return Ok(());
1558                }
1559                (0x20, 0) => {
1560                    // SUBW rd, rs1, x0 → sext.w rd, rs1 (subtract zero)
1561                    self.emit_inst(131); // add_imm_32
1562                    self.emit_data(pvm_rd | (pvm_rs1 << 4));
1563                    self.emit_var_imm(0);
1564                    return Ok(());
1565                }
1566                (0x04, 4) => {
1567                    // PACKW rd, rs1, x0 = ZEXT.H rd, rs1 (Zbb: zero-extend halfword)
1568                    // rd = rs1 & 0xFFFF
1569                    self.emit_inst(70); // and_imm
1570                    self.emit_data(pvm_rd | (pvm_rs1 << 4));
1571                    self.emit_var_imm(0xFFFF);
1572                    return Ok(());
1573                }
1574                _ => {
1575                    return Err(TranspileError::UnsupportedInstruction {
1576                        offset: 0,
1577                        detail: format!("OP-32 x0-as-rs2: funct7={:#x} funct3={}", funct7, funct3),
1578                    });
1579                }
1580            }
1581        }
1582
1583        let pvm_rd = self.require_reg(rd)?;
1584        let pvm_rs1 = self.require_reg(rs1)?;
1585        let pvm_rs2 = self.require_reg(rs2)?;
1586
1587        let pvm_opcode = if funct7 == 1 {
1588            match funct3 {
1589                0 => 192, // MULW → mul_32
1590                4 => 194, // DIVW → div_s_32
1591                5 => 193, // DIVUW → div_u_32
1592                6 => 196, // REMW → rem_s_32
1593                7 => 195, // REMUW → rem_u_32
1594                _ => {
1595                    return Err(TranspileError::UnsupportedInstruction {
1596                        offset: 0,
1597                        detail: format!("OP-32 M funct3={}", funct3),
1598                    });
1599                }
1600            }
1601        } else if funct7 == 0x20 {
1602            match funct3 {
1603                0 => 191, // SUBW → sub_32
1604                5 => 199, // SRAW → shar_r_32
1605                _ => {
1606                    return Err(TranspileError::UnsupportedInstruction {
1607                        offset: 0,
1608                        detail: format!("OP-32 funct7=0x20 funct3={}", funct3),
1609                    });
1610                }
1611            }
1612        } else if funct7 == 0x30 {
1613            // Zbb rolw/rorw
1614            let opc = match funct3 {
1615                1 => 221,
1616                5 => 223,
1617                _ => {
1618                    return Err(TranspileError::UnsupportedInstruction {
1619                        offset: 0,
1620                        detail: format!("OP-32 f7=30 f3={}", funct3),
1621                    });
1622                }
1623            };
1624            self.emit_inst(opc);
1625            self.emit_data(pvm_rs1 | (pvm_rs2 << 4));
1626            self.emit_data(pvm_rd);
1627            return Ok(());
1628        } else if funct7 == 0x04 && funct3 == 4 {
1629            // Zbb zext.h
1630            self.emit_inst(110);
1631            self.emit_data(pvm_rd | (pvm_rs1 << 4));
1632            return Ok(());
1633        } else if funct7 == 0 {
1634            match funct3 {
1635                0 => 190, // ADDW → add_32
1636                1 => 197, // SLLW → shlo_l_32
1637                5 => 198, // SRLW → shlo_r_32
1638                _ => {
1639                    return Err(TranspileError::UnsupportedInstruction {
1640                        offset: 0,
1641                        detail: format!("OP-32 funct3={}", funct3),
1642                    });
1643                }
1644            }
1645        } else {
1646            return Err(TranspileError::UnsupportedInstruction {
1647                offset: 0,
1648                detail: format!("OP-32 funct7={:#x} funct3={}", funct7, funct3),
1649            });
1650        };
1651
1652        // ThreeReg encoding: byte1 = rA | (rB << 4), byte2 = rD
1653        self.emit_inst(pvm_opcode);
1654        self.emit_data(pvm_rs1 | (pvm_rs2 << 4));
1655        self.emit_data(pvm_rd);
1656
1657        Ok(())
1658    }
1659
1660    // ===== Helpers =====
1661
1662    pub(crate) fn require_reg(&self, rv_reg: u8) -> Result<u8, TranspileError> {
1663        match map_register(rv_reg)? {
1664            Some(r) => Ok(r),
1665            None => Ok(0), // x0 → use reg 0 and ignore writes
1666        }
1667    }
1668
1669    pub(crate) fn emit_inst(&mut self, opcode: u8) {
1670        self.code.push(opcode);
1671        self.bitmask.push(1);
1672    }
1673
1674    pub(crate) fn emit_data(&mut self, byte: u8) {
1675        self.code.push(byte);
1676        self.bitmask.push(0);
1677    }
1678
1679    pub(crate) fn emit_imm32(&mut self, imm: i32) {
1680        let bytes = imm.to_le_bytes();
1681        for b in &bytes {
1682            self.emit_data(*b);
1683        }
1684    }
1685
1686    /// Emit a signed immediate using the minimum byte width.
1687    /// PVM instruction categories OneRegOneImm and TwoRegOneImm derive
1688    /// immediate length from the instruction skip distance, so shorter
1689    /// encodings are automatically decoded correctly via sign extension.
1690    /// Compute the number of bytes needed for a variable-length immediate.
1691    fn var_imm_byte_count(imm: i32) -> usize {
1692        if imm == 0 {
1693            0
1694        } else if (-128..=127).contains(&imm) {
1695            1
1696        } else if (-32768..=32767).contains(&imm) {
1697            2
1698        } else {
1699            4
1700        }
1701    }
1702
1703    pub(crate) fn emit_var_imm(&mut self, imm: i32) {
1704        if imm == 0 {
1705            // Zero bytes — decoder gets lx=0, sign_extend(0, 0) = 0
1706        } else if (-128..=127).contains(&imm) {
1707            self.emit_data(imm as i8 as u8);
1708        } else if (-32768..=32767).contains(&imm) {
1709            let bytes = (imm as i16).to_le_bytes();
1710            for b in &bytes {
1711                self.emit_data(*b);
1712            }
1713        } else {
1714            let bytes = imm.to_le_bytes();
1715            for b in &bytes {
1716                self.emit_data(*b);
1717            }
1718        }
1719    }
1720
1721    pub(crate) fn emit_load_imm(&mut self, rd: u8, imm: i64) -> Result<(), TranspileError> {
1722        if rd == 0 {
1723            return Ok(());
1724        } // Write to zero register is nop
1725        let pvm_rd = self.require_reg(rd)?;
1726
1727        if imm >= i32::MIN as i64 && imm <= i32::MAX as i64 {
1728            // load_imm (opcode 51)
1729            self.emit_inst(51);
1730            self.emit_data(pvm_rd);
1731            self.emit_var_imm(imm as i32);
1732        } else {
1733            // load_imm_64 (opcode 20)
1734            self.emit_inst(20);
1735            self.emit_data(pvm_rd);
1736            let bytes = (imm as u64).to_le_bytes();
1737            for b in &bytes {
1738                self.emit_data(*b);
1739            }
1740        }
1741        Ok(())
1742    }
1743
1744    pub(crate) fn emit_jump(&mut self, target: u64) {
1745        let inst_pc = self.code.len() as u32;
1746        self.emit_inst(40); // jump
1747        let fixup_pos = self.code.len();
1748        self.fixups.push((fixup_pos, target, 4));
1749        self.fixup_pcs.insert(fixup_pos, inst_pc);
1750        self.emit_imm32(0); // placeholder
1751    }
1752
1753    /// Emit load_imm_jump: fuse load_imm + jump into a single PVM instruction.
1754    /// Opcode 80: OneRegImmOffset format — sets register and jumps in one step.
1755    /// Saves one instruction (and one gas block boundary) per function call.
1756    fn emit_load_imm_jump(&mut self, rd: u8, imm: i64, target: u64) -> Result<(), TranspileError> {
1757        let pvm_rd = self.require_reg(rd)?;
1758
1759        let inst_pc = self.code.len() as u32;
1760        self.emit_inst(80); // load_imm_jump
1761
1762        // OneRegImmOffset encoding: reg_byte (rd + lX), then imm bytes, then offset bytes.
1763        // Use variable-length encoding for the immediate.
1764        let imm_len = Self::var_imm_byte_count(imm as i32);
1765        let reg_byte = pvm_rd | ((imm_len as u8) << 4);
1766        self.emit_data(reg_byte);
1767        self.emit_var_imm(imm as i32);
1768
1769        // Offset (4 bytes, patched by fixup)
1770        let fixup_pos = self.code.len();
1771        self.fixups.push((fixup_pos, target, 4));
1772        self.fixup_pcs.insert(fixup_pos, inst_pc);
1773        self.emit_imm32(0); // placeholder offset
1774        Ok(())
1775    }
1776
1777    /// Emit a function call: load return address into rd and jump to target.
1778    /// Uses load_imm_jump (opcode 80) to fuse into a single PVM instruction,
1779    /// saving one instruction per call site.
1780    pub(crate) fn emit_call(
1781        &mut self,
1782        rd: u8,
1783        rv_ret_addr: u64,
1784        target: u64,
1785    ) -> Result<(), TranspileError> {
1786        if rd == 0 {
1787            // No return address needed — just jump
1788            self.emit_jump(target);
1789            return Ok(());
1790        }
1791        let jt_idx = self.jump_table.len();
1792        self.jump_table.push(0); // placeholder
1793        self.return_fixups.push((jt_idx, rv_ret_addr));
1794        let jt_addr = ((jt_idx + 1) * 2) as i64;
1795        self.emit_load_imm_jump(rd, jt_addr, target)
1796    }
1797
1798    pub(crate) fn emit_ecalli(&mut self, id: u32) {
1799        self.emit_inst(10);
1800        self.emit_var_imm(id as i32);
1801    }
1802
1803    /// Emit PVM ecall (opcode 3, NoArgs). Management ops + dynamic CALL.
1804    /// φ[11]=op, φ[12]=subject|object — all operands in registers.
1805    pub(crate) fn emit_ecall(&mut self) {
1806        self.emit_inst(3);
1807    }
1808
1809    /// Emit a OneRegImmOffset instruction (used by branch_*_imm opcodes).
1810    ///
1811    /// PVM encoding: [opcode][ra | (lx << 4)][imm (lx bytes LE)][offset (4 bytes LE)]
1812    /// where lx = minimum bytes to represent the signed immediate.
1813    fn emit_branch_imm(&mut self, opcode: u8, ra: u8, imm: i32, target: u64) {
1814        let inst_pc = self.code.len() as u32;
1815        self.emit_inst(opcode);
1816
1817        let (lx, imm_bytes) = encode_var_imm(imm);
1818
1819        // Pack register and immediate length into one byte
1820        self.emit_data(ra | (lx << 4));
1821
1822        // Emit immediate bytes
1823        for b in &imm_bytes {
1824            self.emit_data(*b);
1825        }
1826
1827        // Emit offset placeholder (4 bytes, filled by fixup)
1828        let fixup_pos = self.code.len();
1829        self.fixups.push((fixup_pos, target, 4));
1830        self.fixup_pcs.insert(fixup_pos, inst_pc);
1831        self.emit_imm32(0);
1832    }
1833
1834    /// Build a mapping from RISC-V code addresses to PVM jump table addresses.
1835    ///
1836    /// Only creates jump table entries for the given `targets` — the set of
1837    /// RISC-V addresses actually referenced as function pointers (e.g. from
1838    /// Check whether an address falls within any RISC-V code section.
1839    pub fn is_code_addr(&self, addr: u64) -> bool {
1840        self.code_ranges
1841            .iter()
1842            .any(|(lo, hi)| addr >= *lo && addr < *hi)
1843    }
1844
1845    /// absolute relocations in data sections like vtables). Returns a map of
1846    /// RISC-V address → jump table address (= (index+1)*2).
1847    ///
1848    /// This is needed to fix indirect calls through function pointers stored in
1849    /// data sections (vtables, callbacks, etc.). The PVM's `jump_ind` instruction
1850    /// expects jump table addresses, not raw code offsets.
1851    pub fn build_function_pointer_map(
1852        &mut self,
1853        targets: &std::collections::HashSet<u64>,
1854    ) -> std::collections::HashMap<u64, u32> {
1855        let mut rv_to_jt: std::collections::HashMap<u64, u32> = std::collections::HashMap::new();
1856
1857        let mut target_addrs: Vec<u64> = targets.iter().copied().collect();
1858        target_addrs.sort();
1859
1860        for rv_addr in &target_addrs {
1861            if let Some(&pvm_offset) = self.address_map.get(rv_addr) {
1862                let jt_idx = self.jump_table.len();
1863                self.jump_table.push(pvm_offset);
1864                let jt_addr = ((jt_idx + 1) * 2) as u32;
1865                rv_to_jt.insert(*rv_addr, jt_addr);
1866            }
1867        }
1868
1869        rv_to_jt
1870    }
1871
1872    pub(crate) fn apply_fixups(&mut self) {
1873        // PC-relative fixups (branches, jumps)
1874        for (pvm_offset, rv_target, size) in self.fixups.drain(..).collect::<Vec<_>>() {
1875            if let Some(&pvm_target) = self.address_map.get(&rv_target) {
1876                let inst_pc = self
1877                    .fixup_pcs
1878                    .get(&pvm_offset)
1879                    .copied()
1880                    .unwrap_or(pvm_offset as u32 - 1);
1881                let relative = (pvm_target as i64 - inst_pc as i64) as i32;
1882                let bytes = relative.to_le_bytes();
1883                self.code[pvm_offset..pvm_offset + size as usize]
1884                    .copy_from_slice(&bytes[..size as usize]);
1885            } else {
1886                tracing::warn!(
1887                    "unresolved fixup: rv_target={:#x}, pvm_offset={}",
1888                    rv_target,
1889                    pvm_offset
1890                );
1891            }
1892        }
1893
1894        // Resolve return address fixups in the jump table
1895        let mut unresolved = 0usize;
1896        for (jt_idx, rv_addr) in self.return_fixups.drain(..).collect::<Vec<_>>() {
1897            if let Some(&pvm_target) = self.address_map.get(&rv_addr) {
1898                self.jump_table[jt_idx] = pvm_target;
1899            } else {
1900                unresolved += 1;
1901                eprintln!(
1902                    "javm-transpiler: unresolved return_fixup jt_idx={jt_idx} rv_addr={:#x}",
1903                    rv_addr
1904                );
1905            }
1906        }
1907        if unresolved > 0 {
1908            eprintln!(
1909                "javm-transpiler: {unresolved} unresolved return_fixups (will panic on jump)"
1910            );
1911        }
1912    }
1913}
1914
1915// ===== RISC-V immediate decoders =====
1916
1917fn decode_j_imm(inst: u32) -> i32 {
1918    let imm20 = (inst >> 31) & 1;
1919    let imm10_1 = (inst >> 21) & 0x3FF;
1920    let imm11 = (inst >> 20) & 1;
1921    let imm19_12 = (inst >> 12) & 0xFF;
1922    let imm = (imm20 << 20) | (imm19_12 << 12) | (imm11 << 11) | (imm10_1 << 1);
1923    // Sign extend from bit 20
1924    if imm20 != 0 {
1925        (imm | 0xFFE00000) as i32
1926    } else {
1927        imm as i32
1928    }
1929}
1930
1931fn decode_b_imm(inst: u32) -> i32 {
1932    let imm12 = (inst >> 31) & 1;
1933    let imm10_5 = (inst >> 25) & 0x3F;
1934    let imm4_1 = (inst >> 8) & 0xF;
1935    let imm11 = (inst >> 7) & 1;
1936    let imm = (imm12 << 12) | (imm11 << 11) | (imm10_5 << 5) | (imm4_1 << 1);
1937    if imm12 != 0 {
1938        (imm | 0xFFFFE000) as i32
1939    } else {
1940        imm as i32
1941    }
1942}
1943
1944fn decode_s_imm(inst: u32) -> i32 {
1945    let imm11_5 = (inst >> 25) & 0x7F;
1946    let imm4_0 = (inst >> 7) & 0x1F;
1947    let imm = (imm11_5 << 5) | imm4_0;
1948    if imm11_5 & 0x40 != 0 {
1949        (imm | 0xFFFFF000) as i32
1950    } else {
1951        imm as i32
1952    }
1953}
1954
1955#[cfg(test)]
1956mod tests {
1957    use super::*;
1958
1959    #[test]
1960    fn test_register_mapping() {
1961        assert_eq!(map_register(0).unwrap(), None); // zero
1962        assert_eq!(map_register(1).unwrap(), Some(0)); // ra
1963        assert_eq!(map_register(2).unwrap(), Some(1)); // sp
1964        assert_eq!(map_register(10).unwrap(), Some(7)); // a0
1965        assert_eq!(map_register(15).unwrap(), Some(12)); // a5
1966        assert!(map_register(3).is_err()); // gp: no mapping
1967    }
1968
1969    #[test]
1970    fn test_decode_j_imm() {
1971        // JAL x0, 0 (forward)
1972        assert_eq!(decode_j_imm(0x0000006F), 0);
1973        // JAL x0, 4
1974        assert_eq!(decode_j_imm(0x0040006F), 4);
1975    }
1976
1977    #[test]
1978    fn test_decode_j_imm_negative() {
1979        // JAL with negative offset (backward jump)
1980        // imm[20|10:1|11|19:12] = all 1s → -2
1981        // Encoding: bit31=1, bits[30:21]=0x3FF, bit20=1, bits[19:12]=0xFF
1982        let inst = 0xFFFFF06F; // JAL x0, -2 (the standard encoding for JAL x0, -2)
1983        let imm = decode_j_imm(inst);
1984        assert!(imm < 0, "negative offset should be negative");
1985        assert_eq!(imm, -2);
1986    }
1987
1988    #[test]
1989    fn test_decode_j_imm_max_positive() {
1990        // JAL with maximum positive offset: imm[20]=0, rest=max
1991        // imm = 0x0FFFFE (1048574)
1992        let inst = 0x7FFFF06F; // bit31=0, bits[30:21]=0x3FF, bit20=1, bits[19:12]=0xFF
1993        let imm = decode_j_imm(inst);
1994        assert!(imm > 0);
1995    }
1996
1997    #[test]
1998    fn test_decode_b_imm_zero() {
1999        // BEQ x0, x0, 0
2000        assert_eq!(decode_b_imm(0x00000063), 0);
2001    }
2002
2003    #[test]
2004    fn test_decode_b_imm_positive() {
2005        // BEQ with offset +8: imm[12|10:5]=0, imm[4:1]=0100, imm[11]=0
2006        // inst[11:8]=0100 → imm[4:1]=4, shift by 1 → offset=8
2007        let inst = 0x00000463; // BEQ x0, x0, +8
2008        let imm = decode_b_imm(inst);
2009        assert_eq!(imm, 8);
2010    }
2011
2012    #[test]
2013    fn test_decode_b_imm_negative() {
2014        // BEQ with negative offset
2015        // All bits set → -2
2016        let inst = 0xFE000FE3; // BEQ x0, x0, -2
2017        let imm = decode_b_imm(inst);
2018        assert_eq!(imm, -2);
2019    }
2020
2021    #[test]
2022    fn test_decode_s_imm_zero() {
2023        // SD x0, 0(x0): imm=0
2024        assert_eq!(decode_s_imm(0x00003023), 0);
2025    }
2026
2027    #[test]
2028    fn test_decode_s_imm_positive() {
2029        // SD x0, 8(x0): imm[11:5]=0, imm[4:0]=01000
2030        let inst = 0x00003423; // imm4_0 = 8 (at bits[11:7] = 01000)
2031        let imm = decode_s_imm(inst);
2032        assert_eq!(imm, 8);
2033    }
2034
2035    #[test]
2036    fn test_decode_s_imm_negative() {
2037        // SD x0, -8(x0): imm = -8 = 0xFFF8
2038        // imm[11:5] = 1111111, imm[4:0] = 11000
2039        let inst = 0xFE003C23; // SD x0, -8(x0)
2040        let imm = decode_s_imm(inst);
2041        assert_eq!(imm, -8);
2042    }
2043
2044    #[test]
2045    fn test_register_mapping_all_a_regs() {
2046        // a0-a5 map to PVM registers 7-12
2047        for (rv, pvm) in [(10, 7), (11, 8), (12, 9), (13, 10), (14, 11), (15, 12)] {
2048            assert_eq!(
2049                map_register(rv).unwrap(),
2050                Some(pvm),
2051                "rv{rv} should map to φ[{pvm}]"
2052            );
2053        }
2054    }
2055
2056    #[test]
2057    fn test_register_mapping_s_regs() {
2058        // s0-s1 map to PVM registers 5-6
2059        assert_eq!(map_register(8).unwrap(), Some(5)); // s0
2060        assert_eq!(map_register(9).unwrap(), Some(6)); // s1
2061    }
2062
2063    #[test]
2064    fn test_register_mapping_unmapped() {
2065        // tp(4), t3-t6(28-31) are unmapped → error
2066        assert!(map_register(4).is_err()); // tp
2067        assert!(map_register(28).is_err()); // t3
2068    }
2069}