Skip to main content

javm_exec/
predecode.rs

1//! Single-pass predecode for PVM2 (RV+C+custom-0) byte streams.
2//!
3//! Walks the code from PC=0, decoding every instruction and recording:
4//!
5//! - **Decoded instruction array** (`Vec<RvPreDecodedInst>`): one entry
6//!   per static instruction, with PC + next-PC pre-computed so the
7//!   codegen loop doesn't redo decoding.
8//! - **Gas-block-start markers**: PC=0 plus every PC immediately
9//!   following a terminator. The strict post-terminator set is sound
10//!   because runtime `jalr` targets are validated to be block starts
11//!   (the linker injects a `Fallthrough` terminator before any branch
12//!   target that isn't naturally post-terminator).
13//!
14//! Per-block gas costs are computed by running the pipeline simulator
15//! in [`crate::gas_cost::rv_gas_cost_for_block`] once per basic block;
16//! the results are stored in [`Predecode::block_costs`].
17
18use crate::instruction::{Inst, decode};
19use alloc::vec;
20use alloc::vec::Vec;
21
22/// Pre-resolved metadata used by the per-block gas accountant. The
23/// fields are computed once at decode time so the gas hot path does
24/// not have to re-match the `Inst` variant on each invocation.
25///
26/// - `kind` is an index into `gas_cost::RV_GAS_COST_LUT`.
27/// - `src1_slot`/`src2_slot`/`dst_slot` are PVM2 register slots
28///   (0..12, ordered x1, x2, x5..x15) or `0xFF` for "no register"
29///   (x0, x3, x4, or an unused register slot for this opcode).
30#[derive(Debug, Clone, Copy, Default)]
31pub struct RvGasMeta {
32    pub kind: u8,
33    pub src1_slot: u8,
34    pub src2_slot: u8,
35    pub dst_slot: u8,
36}
37
38/// One decoded instruction with its PC, next-PC, block-start flag,
39/// and pre-resolved gas metadata.
40#[derive(Debug, Clone, Copy)]
41pub struct RvPreDecodedInst {
42    pub inst: Inst,
43    pub pc: u32,
44    pub next_pc: u32,
45    pub is_gas_block_start: bool,
46    pub gas_meta: RvGasMeta,
47}
48
49/// Output of the predecode pass over an RV+C+custom-0 code section.
50#[derive(Debug, Clone)]
51pub struct Predecode {
52    /// One entry per static instruction.
53    pub insts: Vec<RvPreDecodedInst>,
54    /// Pre-computed per-basic-block gas cost. Aligned with `insts`:
55    /// `block_costs[i]` is meaningful only when
56    /// `insts[i].is_gas_block_start == true`; entries at non-
57    /// block-start indices are 0. Each meaningful entry is
58    /// `max(simulation_cycles - 3, 1)` for the block starting at
59    /// that instruction, computed by the pipeline simulator in
60    /// `gas_cost::rv_gas_cost_for_block`.
61    pub block_costs: Vec<u32>,
62    /// Pre-computed per-basic-block worst-case category-#3 reserve.
63    /// Aligned with `insts` exactly like `block_costs`: meaningful only
64    /// at block-start indices, 0 elsewhere. The block-entry gate checks
65    /// `gas ≥ block_costs[i] + block_reserves[i]` but charges only
66    /// `block_costs[i]` — the reserve is a gate, not a charge (see
67    /// `gas_cost::rv_block_reserve_for_block`).
68    pub block_reserves: Vec<u32>,
69    /// If decode hit a reserved/illegal encoding, the byte offset of
70    /// the first one. `None` on success.
71    pub decode_error_at: Option<u32>,
72}
73
74/// Predecode an entire RV+C+custom-0 code section.
75///
76/// Linear pass; no recursion, no bitmask consultation. The
77/// self-describing length encoding (`op[1:0]` tells you 2-byte vs
78/// 4-byte) makes every advance unambiguous.
79///
80/// Per-block gas costs are computed by running the pipeline
81/// simulator from `gas_cost::rv_gas_cost_for_block` over every basic
82/// block. `mem_cycles` is the load/store cycle latency for the
83/// active memory tier (mirrors `DEFAULT_MEM_CYCLES = 25` from the
84/// PVM gas table).
85pub fn predecode(code: &[u8]) -> Predecode {
86    predecode_rv_with_mem_cycles(code, crate::gas_cost::DEFAULT_MEM_CYCLES)
87}
88
89/// Like `predecode` but takes an explicit `mem_cycles` parameter.
90/// Used by callers that want to override the default L2-hit latency
91/// (e.g. for tier-specific gas modeling).
92pub fn predecode_rv_with_mem_cycles(code: &[u8], mem_cycles: u8) -> Predecode {
93    let mut insts: Vec<RvPreDecodedInst> = Vec::with_capacity(code.len() / 4);
94    let mut decode_error_at: Option<u32> = None;
95
96    // ---- Pass 1: decode every instruction ----------------------------
97    let mut pc: usize = 0;
98    while pc < code.len() {
99        let Some((inst, len)) = decode(&code[pc..]) else {
100            decode_error_at = Some(pc as u32);
101            break;
102        };
103        if matches!(inst, Inst::Reserved { .. }) && decode_error_at.is_none() {
104            decode_error_at = Some(pc as u32);
105        }
106        let next_pc = (pc + len as usize) as u32;
107        let gas_meta = crate::gas_cost::rv_gas_meta(&inst);
108        insts.push(RvPreDecodedInst {
109            inst,
110            pc: pc as u32,
111            next_pc,
112            is_gas_block_start: false,
113            gas_meta,
114        });
115        pc = next_pc as usize;
116    }
117
118    // ---- Pass 2: mark gas-block starts (strict post-terminator) ------
119    //
120    // The set of legal gas-block-starts is:
121    //
122    //     {0} ∪ { pc | pc immediately follows a terminator instruction }
123    //
124    // This strict set is sound because indirect control flow (`jalr`)
125    // is validated at runtime to land on a block start, and the linker
126    // injects `Fallthrough` (a terminator no-op) before any branch
127    // target that isn't naturally post-terminator. So every
128    // statically- or dynamically-reachable target lands in this set.
129    //
130    // OOG happens at the per-block gas check at the block start, so
131    // a paused PC is always in this set.
132    if !insts.is_empty() {
133        insts[0].is_gas_block_start = true;
134    }
135    for i in 0..insts.len() {
136        // Post-terminator instructions start a block (the usual rule).
137        if i > 0 && is_terminator(&insts[i - 1].inst) {
138            insts[i].is_gas_block_start = true;
139        }
140        // `ecall.jar` / `ecalli` are *forced* block starts — each is its
141        // own singleton gas block with no static preamble, charged
142        // dynamically at dispatch. This makes the ecall's own PC a
143        // bb_start so an OOG on its dynamic charge can re-attempt there
144        // (see ~/docs/spec-staging/gas-cost.md §3 "ecall/ecalli blocks").
145        // It is a terminator too, so the instruction after it is already
146        // a block start — the ecall block is a boundary on both sides.
147        if is_ecall_block(&insts[i].inst) {
148            insts[i].is_gas_block_start = true;
149        }
150    }
151
152    // ---- Pass 3: per-block gas costs + #3 reserves ------------------
153    let (block_costs, block_reserves) = compute_block_costs(&insts, mem_cycles);
154
155    Predecode {
156        insts,
157        block_costs,
158        block_reserves,
159        decode_error_at,
160    }
161}
162
163/// Run the pipeline simulator once per basic block and compute the
164/// worst-case #3 reserve; write both into the block-start index for
165/// each gas-block-start. Non-block-start indices remain 0.
166fn compute_block_costs(insts: &[RvPreDecodedInst], mem_cycles: u8) -> (Vec<u32>, Vec<u32>) {
167    let mut block_costs = vec![0u32; insts.len()];
168    let mut block_reserves = vec![0u32; insts.len()];
169    for i in 0..insts.len() {
170        if insts[i].is_gas_block_start {
171            if is_ecall_block(&insts[i].inst) {
172                // ecall/ecalli block: cost == 0 is the sentinel both
173                // engines use to skip the static gate; the kernel charges
174                // it dynamically at dispatch (its #3 is the call-frame /
175                // host cost, not a static load/store reserve).
176                block_costs[i] = 0;
177                block_reserves[i] = 0;
178            } else {
179                block_costs[i] = crate::gas_cost::rv_gas_cost_for_block(insts, i, mem_cycles);
180                block_reserves[i] = crate::gas_cost::rv_block_reserve_for_block(insts, i);
181            }
182        }
183    }
184    (block_costs, block_reserves)
185}
186
187/// `ecall.jar` / `ecalli` — the instructions that form their own
188/// (singleton) gas block, charged dynamically at dispatch rather than by
189/// a static block preamble. A `cost == 0` block start marks one.
190#[inline]
191pub fn is_ecall_block(inst: &Inst) -> bool {
192    matches!(inst, Inst::EcallJar | Inst::Ecalli { .. })
193}
194
195/// Block-terminating instructions: anything that *can* leave the
196/// fall-through path. Used to mark the next instruction as a
197/// gas-block start.
198pub fn is_terminator(inst: &Inst) -> bool {
199    matches!(
200        inst,
201        // PC-relative jumps.
202        Inst::Jal { .. }
203            // Indirect jumps (returns, indirect calls).
204            | Inst::Jalr { .. }
205            // Static branches.
206            | Inst::Beq { .. }
207            | Inst::Bne { .. }
208            | Inst::Blt { .. }
209            | Inst::Bge { .. }
210            | Inst::Bltu { .. }
211            | Inst::Bgeu { .. }
212            // Custom-0 control transfers.
213            | Inst::Trap
214            | Inst::EcallJar
215            | Inst::Ecalli { .. }
216            | Inst::Fallthrough
217            // Reserved encodings panic at runtime.
218            | Inst::Reserved { .. }
219    )
220}
221
222// Per-instruction gas is computed by pipeline-aware per-block
223// simulation, not a flat per-op sum. See
224// `gas_cost::rv_gas_cost_for_block` for the block-cost computation.
225// Costs are precomputed in `predecode` and returned via
226// `Predecode::block_costs`.
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231    use alloc::vec;
232
233    fn enc(insns: &[u32]) -> Vec<u8> {
234        let mut v = Vec::with_capacity(insns.len() * 4);
235        for w in insns {
236            v.extend_from_slice(&w.to_le_bytes());
237        }
238        v
239    }
240
241    #[test]
242    fn empty_code_yields_empty() {
243        let r = predecode(&[]);
244        assert!(r.insts.is_empty());
245        assert!(r.decode_error_at.is_none());
246    }
247
248    #[test]
249    fn linear_sequence() {
250        // addi x10, x11, 1 ; addi x10, x10, 2 ; jal x0, -8 (loop back)
251        let code = enc(&[
252            0x00158513, // addi x10, x11, 1
253            0x00250513, // addi x10, x10, 2
254            // jal x0, -8: J-type imm = -8 bytes
255            // J encoding: imm[20|10:1|11|19:12] | rd | opcode
256            // imm = -8 = 0xFFFFFFF8 = 1111...11111000 in 21-bit
257            // Manually: jal x0, .-8 = 0xFF9FF06F
258            0xFF9FF06F,
259        ]);
260        let r = predecode(&code);
261        assert_eq!(r.insts.len(), 3);
262        assert_eq!(r.insts[0].pc, 0);
263        assert_eq!(r.insts[0].next_pc, 4);
264        assert_eq!(r.insts[1].pc, 4);
265        assert_eq!(r.insts[1].next_pc, 8);
266        assert_eq!(r.insts[2].pc, 8);
267        assert_eq!(r.insts[2].next_pc, 12);
268        // PC=0 always a block start; target of the jal (at PC=0) marks insts[0]; no others
269        assert!(r.insts[0].is_gas_block_start);
270        // No reserved encodings.
271        assert!(r.decode_error_at.is_none());
272    }
273
274    #[test]
275    fn post_terminator_is_block_start_branch_target_isnt() {
276        // 0: beq x0, x0, 8  (terminator — post-PC=4 is block start)
277        // 4: addi x10, x10, 1  (post-terminator — block start)
278        // 8: addi x11, x11, 2  (branch target — NOT post-terminator,
279        //                       so not a block start in strict mode;
280        //                       the linker would inject `fallthrough`
281        //                       before PC=8 in a real build)
282        let beq = 0x00000463u32; // beq x0, x0, +8
283        let code = enc(&[beq, 0x00150513, 0x00158593]);
284        let r = predecode(&code);
285        assert_eq!(r.insts.len(), 3);
286        // PC=0 always block start.
287        assert!(r.insts[0].is_gas_block_start);
288        // Post-beq → PC=4 is block start.
289        assert!(r.insts[1].is_gas_block_start);
290        // PC=8 is a branch target but the previous instruction (addi at
291        // PC=4) is not a terminator. The linker is responsible for
292        // injecting `fallthrough` before such targets in real builds;
293        // the predecode pass itself doesn't infer it.
294        assert!(!r.insts[2].is_gas_block_start);
295    }
296
297    #[test]
298    fn fallthrough_creates_block_start() {
299        // 0: addi x10, x10, 1   (not a terminator)
300        // 4: fallthrough         (terminator — post-PC=8 is block start)
301        // 8: addi x11, x11, 2   (post-fallthrough — block start)
302        // custom-0 with funct3=0b100 is the fallthrough encoding.
303        // Major opcode = 0b00_010 (0x2), bits[1:0]=11.
304        // Word = (funct3<<12) | (op_custom_0<<2) | 0b11 = (4<<12) | (2<<2) | 3 = 0x400B
305        let fallthrough_word = 0x0000_400Bu32;
306        let code = enc(&[0x00150513, fallthrough_word, 0x00158593]);
307        let r = predecode(&code);
308        assert_eq!(r.insts.len(), 3);
309        assert!(r.insts[0].is_gas_block_start); // PC=0
310        assert!(!r.insts[1].is_gas_block_start); // PC=4 post-addi (not terminator)
311        assert!(r.insts[2].is_gas_block_start); // PC=8 post-fallthrough
312        assert!(matches!(r.insts[1].inst, Inst::Fallthrough));
313    }
314
315    #[test]
316    fn jalr_is_terminator() {
317        // jalr x0, x1, 0 (= the `ret` idiom). I-type: rd=0, rs1=1,
318        // imm=0, funct3=0, opcode=1100111 → 0x00008067.
319        let jalr = 0x00008067u32;
320        let code = enc(&[jalr, 0x00150513]);
321        let r = predecode(&code);
322        assert_eq!(r.insts.len(), 2);
323        assert!(matches!(
324            r.insts[0].inst,
325            Inst::Jalr {
326                rd: 0,
327                rs1: 1,
328                imm: 0
329            }
330        ));
331        assert!(r.insts[0].is_gas_block_start); // PC=0
332        assert!(r.insts[1].is_gas_block_start); // post-jalr (terminator)
333        assert_eq!(r.decode_error_at, None);
334    }
335
336    #[test]
337    fn auipc_decodes() {
338        // auipc x10, 0x12 → rd=10, imm=(0x12<<12). opcode=0010111.
339        // word = (0x12 << 12) | (10 << 7) | 0b0010111 = 0x00012517
340        let auipc = 0x00012517u32;
341        let code = enc(&[auipc]);
342        let r = predecode(&code);
343        assert_eq!(r.insts.len(), 1);
344        assert!(matches!(
345            r.insts[0].inst,
346            Inst::Auipc {
347                rd: 10,
348                imm: 0x12000
349            }
350        ));
351        // auipc is a plain ALU op, NOT a terminator.
352        assert_eq!(r.decode_error_at, None);
353    }
354
355    #[test]
356    fn reserved_encoding_recorded() {
357        // ecall (standard) = 0x00000073 → Reserved
358        let code = enc(&[0x00000073]);
359        let r = predecode(&code);
360        assert_eq!(r.insts.len(), 1);
361        assert_eq!(r.decode_error_at, Some(0));
362        assert!(matches!(r.insts[0].inst, Inst::Reserved { .. }));
363    }
364
365    #[test]
366    fn compressed_then_standard() {
367        // c.li x10, 5 (2 bytes) ; addi x11, x11, 1 (4 bytes)
368        // c.li x10, 5: per spec CI imm6=5, rd=10, opcode=01, funct3=010
369        // h = (0<<12) | (10<<7) | (5<<2) | 0b01 with f3=010 in bits 15:13
370        // = (0b010 << 13) | (10 << 7) | (5 << 2) | 0b01
371        // = 0x4000 | 0x500 | 0x14 | 0x01 = 0x4515
372        let cli = 0x4515u16.to_le_bytes();
373        let mut code = vec![cli[0], cli[1]];
374        code.extend_from_slice(&0x00158593u32.to_le_bytes());
375        let r = predecode(&code);
376        assert_eq!(r.insts.len(), 2);
377        assert_eq!(r.insts[0].pc, 0);
378        assert_eq!(r.insts[0].next_pc, 2);
379        assert_eq!(r.insts[1].pc, 2);
380        assert_eq!(r.insts[1].next_pc, 6);
381    }
382}