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}