Skip to main content

javm_exec/
gas_cost.rs

1//! Per-basic-block gas cost model for PVM2 (JAR v0.8.0).
2//!
3//! Simulates a single-pass CPU pipeline to compute gas cost for a
4//! basic block. Cost = `max(simulation_cycles - 3, 1)`.
5//!
6//! Pipeline model:
7//! - Reorder buffer: max 32 entries
8//! - 4 decode slots per cycle, 5 dispatch slots per cycle
9//! - Execution units: ALU:4, LOAD:4, STORE:4, MUL:1, DIV:1
10//!
11//! Per-opcode pipeline metadata lives in `RV_GAS_COST_LUT`, indexed by
12//! the per-kind constants below (`RV_KIND_*`). The interpreter and
13//! recompiler share this LUT via [`rv_feed_gas_kind`] / [`rv_feed_gas_direct`].
14
15#![allow(dead_code)] // some helpers are used only by the recompiler crate
16
17/// `FastCost` is the per-instruction analysis the gas simulator
18/// consumes. Cycles, decode slots, exec-unit class, src/dst register
19/// masks. `is_terminator` marks gas-block-ending instructions.
20#[derive(Clone, Copy, Debug)]
21pub struct FastCost {
22    pub cycles: u8,
23    pub decode_slots: u8,
24    /// 0=none, 1=alu, 2=load(+alu), 3=store(+alu), 4=mul(+alu), 5=div(+alu)
25    pub exec_unit: u8,
26    pub src_mask: u16,
27    pub dst_mask: u16,
28    pub is_terminator: bool,
29    pub is_move_reg: bool,
30}
31
32const EU_NONE: u8 = 0;
33const EU_ALU: u8 = 1;
34const EU_LOAD: u8 = 2;
35const EU_STORE: u8 = 3;
36const EU_MUL: u8 = 4;
37const EU_DIV: u8 = 5;
38
39/// Default load/store latency (L2 cache hit baseline).
40pub const DEFAULT_MEM_CYCLES: u8 = 25;
41
42// ============================================================================
43// PVM2 fast-path gas accounting (LUT + feed_direct)
44// ============================================================================
45//
46// Mirrors PVM's `feed_gas_direct` / `GAS_COST_LUT` optimization for the
47// PVM2 path. The hot per-instruction cost is computed via:
48//   1. `rv_op_metadata(&inst)` → `(kind: u8, rs1, rs2, rd)` — one
49//      match over `Inst` variants, returning a u32-packed tuple.
50//   2. `RV_GAS_COST_LUT[kind]` — static array lookup giving cycles,
51//      decode_slots, exec_unit, reg patterns, and overlap flags.
52//   3. `GasSimulator::feed_direct(cycles, decode_slots, src1, src2, dst)`
53//      — bypasses `FastCost` construction and the bitmask iteration in
54//      `feed`.
55//
56// PVM2 doesn't need a "needs_full" slow path: branches use a flat 20
57// (no target-opcode dependency), and overlap-dependent decode_slots
58// are computed inline from the LUT entry's `overlap_slots` nibbles.
59
60/// PVM2-side flags for `RvGasCostEntry::flags`.
61const RVF_TERM: u8 = 1;
62/// `decode_slots` depends on whether `rd` overlaps any source register
63/// in `src_pat`. Lo nibble of `overlap_slots` is the value when the
64/// overlap holds; hi nibble is the value when it doesn't.
65const RVF_OVERLAP_DST_SRC: u8 = 2;
66/// `decode_slots` depends on whether `rs1 == rd` (used by shifts and
67/// rotates — the existing PVM shift rule). Same overlap-slots layout.
68const RVF_OVERLAP_RS1_RD: u8 = 4;
69
70#[derive(Clone, Copy)]
71struct RvGasCostEntry {
72    /// Execution latency (cycles). For LOAD/STORE rows this is
73    /// overridden at lookup time by `mem_cycles`.
74    cycles: u8,
75    /// Base decode_slots (used when no overlap flag is set).
76    decode_slots: u8,
77    /// Execution unit class (`EU_*` constants from this file).
78    exec_unit: u8,
79    /// Source register pattern: 0=none, 1=rs1, 2=rs1+rs2.
80    src_pat: u8,
81    /// Destination register pattern: 0=none, 1=rd.
82    /// (Writes to x0 are silently treated as "no destination" by
83    /// `rv_slot_u8`.)
84    dst_pat: u8,
85    /// `RVF_*` flag bits.
86    flags: u8,
87    /// When `RVF_OVERLAP_*` is set: lo nibble = decode_slots when
88    /// overlap holds, hi nibble = decode_slots when it doesn't.
89    overlap_slots: u8,
90}
91
92#[allow(clippy::too_many_arguments)]
93const fn rgc(
94    cycles: u8,
95    decode_slots: u8,
96    exec_unit: u8,
97    src_pat: u8,
98    dst_pat: u8,
99    flags: u8,
100) -> RvGasCostEntry {
101    RvGasCostEntry {
102        cycles,
103        decode_slots,
104        exec_unit,
105        src_pat,
106        dst_pat,
107        flags,
108        overlap_slots: 0,
109    }
110}
111
112#[allow(clippy::too_many_arguments)]
113const fn rgc_ov(
114    cycles: u8,
115    overlap_if: u8,
116    overlap_no: u8,
117    exec_unit: u8,
118    src_pat: u8,
119    dst_pat: u8,
120    flags: u8,
121) -> RvGasCostEntry {
122    RvGasCostEntry {
123        cycles,
124        decode_slots: 0,
125        exec_unit,
126        src_pat,
127        dst_pat,
128        flags,
129        overlap_slots: overlap_if | (overlap_no << 4),
130    }
131}
132
133// PVM2 opcode kinds. Used as indices into RV_GAS_COST_LUT.
134// All variants of `Inst` that share a gas-cost row map to the same
135// kind (e.g. `Lb`/`Lh`/.../`Lwu` all → `RV_KIND_LOAD`).
136//
137// Exposed to the recompiler so each `compile_rv_instruction` arm can
138// supply its kind constant directly to the gas-feed call, removing a
139// separate match over `Inst` per instruction.
140pub const RV_KIND_RESERVED: u8 = 0;
141pub const RV_KIND_TRAP: u8 = 1;
142pub const RV_KIND_FALLTHROUGH: u8 = 2;
143pub const RV_KIND_ECALL_JAR: u8 = 3;
144pub const RV_KIND_ECALLI: u8 = 4;
145pub const RV_KIND_JALR: u8 = 5; // indirect jump (return / indirect call)
146pub const RV_KIND_FENCE: u8 = 6;
147pub const RV_KIND_JAL: u8 = 7;
148pub const RV_KIND_BRANCH: u8 = 8;
149pub const RV_KIND_LOAD: u8 = 9;
150pub const RV_KIND_STORE: u8 = 10;
151pub const RV_KIND_LUI: u8 = 11;
152pub const RV_KIND_ADDI: u8 = 12; // 64-bit I-type ALU (Addi/Andi/Ori/Xori/Sltiu/Slli/Srli/Slti/Srai)
153pub const RV_KIND_ADDIW: u8 = 13; // 32-bit I-type ALU (Addiw/Slliw/Srliw/Sraiw)
154pub const RV_KIND_ADD: u8 = 14; // 64-bit R-type ALU (Add/Sub/And/Or/Xor)
155pub const RV_KIND_SLL: u8 = 15; // 64-bit shifts (Sll/Srl/Sra)
156pub const RV_KIND_SLT: u8 = 16; // 64-bit compare (Slt/Sltu)
157pub const RV_KIND_ADDW: u8 = 17; // 32-bit R-type ALU (Addw/Subw)
158pub const RV_KIND_SLLW: u8 = 18; // 32-bit shifts (Sllw/Srlw/Sraw)
159pub const RV_KIND_MUL: u8 = 19;
160pub const RV_KIND_MULW: u8 = 20;
161pub const RV_KIND_MULH: u8 = 21; // Mulh/Mulhu
162pub const RV_KIND_MULHSU: u8 = 22;
163pub const RV_KIND_DIV: u8 = 23; // Div/Divu/Rem/Remu + W-variants
164pub const RV_KIND_ZBB_U1: u8 = 24; // Clz/Clzw/Cpop/Cpopw/SextB/SextH/ZextH/Rev8/OrcB
165pub const RV_KIND_ZBB_CTZ: u8 = 25; // Ctz/Ctzw
166pub const RV_KIND_ZBB_MINMAX: u8 = 26; // Min/Minu/Max/Maxu
167pub const RV_KIND_ZBB_INV: u8 = 27; // Andn/Orn (no overlap rule)
168pub const RV_KIND_ZBB_XNOR: u8 = 28; // Xnor (overlap rule)
169pub const RV_KIND_ZBB_ROT: u8 = 29; // Rol/Ror
170pub const RV_KIND_ZBB_RORI: u8 = 30; // Rori (single-src I-type)
171pub const RV_KIND_ZBB_ROTW: u8 = 31; // Rolw/Rorw
172pub const RV_KIND_ZBB_RORIW: u8 = 32; // Roriw
173pub const RV_KIND_ZBA: u8 = 33; // Sh1add..Sh3adduw, Adduw
174pub const RV_KIND_ZBA_IMM: u8 = 34; // Slliuw
175pub const RV_KIND_ZBS: u8 = 35; // Bclr/Bset/Binv/Bext
176pub const RV_KIND_ZBS_IMM: u8 = 36; // Bclri/Bseti/Binvi/Bexti
177pub const RV_KIND_ZICOND: u8 = 37; // CzeroEqz/CzeroNez
178
179const RV_LUT_LEN: usize = 64;
180
181static RV_GAS_COST_LUT: [RvGasCostEntry; RV_LUT_LEN] = {
182    let d = rgc(2, 1, EU_NONE, 0, 0, RVF_TERM); // default — Reserved-shaped
183    let mut t = [d; RV_LUT_LEN];
184    // Custom-0 / no-reg terminators
185    t[RV_KIND_RESERVED as usize] = rgc(2, 1, EU_NONE, 0, 0, RVF_TERM);
186    t[RV_KIND_TRAP as usize] = rgc(2, 1, EU_NONE, 0, 0, RVF_TERM);
187    t[RV_KIND_FALLTHROUGH as usize] = rgc(2, 1, EU_NONE, 0, 0, RVF_TERM);
188    t[RV_KIND_ECALL_JAR as usize] = rgc(100, 4, EU_ALU, 0, 0, RVF_TERM);
189    t[RV_KIND_ECALLI as usize] = rgc(100, 4, EU_ALU, 0, 0, RVF_TERM);
190    // jalr: src = rs1 (target), no tracked dst (terminator — its rd
191    // write has no in-block successor), terminator. 22/1/ALU mirrors
192    // the legacy PVM indirect-jump cost.
193    t[RV_KIND_JALR as usize] = rgc(22, 1, EU_ALU, 1, 0, RVF_TERM);
194    // Fences: no-op
195    t[RV_KIND_FENCE as usize] = rgc(1, 1, EU_NONE, 0, 0, 0);
196    // JAL: src=none, dst=rd, terminator (mirrors PVM jump = 15/1/ALU)
197    t[RV_KIND_JAL as usize] = rgc(15, 1, EU_ALU, 0, 1, RVF_TERM);
198    // Branches: src=rs1+rs2, dst=none, terminator. Flat 20-cycle (PVM2
199    // doesn't have a trap-fast-path because the linker rewrites trap
200    // targets).
201    t[RV_KIND_BRANCH as usize] = rgc(20, 1, EU_ALU, 2, 0, RVF_TERM);
202    // Loads / stores
203    t[RV_KIND_LOAD as usize] = rgc(25, 1, EU_LOAD, 1, 1, 0);
204    t[RV_KIND_STORE as usize] = rgc(25, 1, EU_STORE, 2, 0, 0);
205    // Lui
206    t[RV_KIND_LUI as usize] = rgc(1, 2, EU_NONE, 0, 1, 0);
207    // 64-bit I-type ALU
208    t[RV_KIND_ADDI as usize] = rgc_ov(1, 1, 2, EU_ALU, 1, 1, RVF_OVERLAP_DST_SRC);
209    // 32-bit I-type ALU
210    t[RV_KIND_ADDIW as usize] = rgc_ov(2, 2, 3, EU_ALU, 1, 1, RVF_OVERLAP_DST_SRC);
211    // 64-bit R-type ALU
212    t[RV_KIND_ADD as usize] = rgc_ov(1, 1, 2, EU_ALU, 2, 1, RVF_OVERLAP_DST_SRC);
213    // 64-bit shifts (rs1 == rd rule)
214    t[RV_KIND_SLL as usize] = rgc_ov(1, 2, 3, EU_ALU, 2, 1, RVF_OVERLAP_RS1_RD);
215    // 64-bit comparisons (no overlap)
216    t[RV_KIND_SLT as usize] = rgc(3, 3, EU_ALU, 2, 1, 0);
217    // 32-bit R-type ALU
218    t[RV_KIND_ADDW as usize] = rgc_ov(2, 2, 3, EU_ALU, 2, 1, RVF_OVERLAP_DST_SRC);
219    // 32-bit shifts
220    t[RV_KIND_SLLW as usize] = rgc_ov(2, 3, 4, EU_ALU, 2, 1, RVF_OVERLAP_RS1_RD);
221    // Multiplies
222    t[RV_KIND_MUL as usize] = rgc_ov(3, 1, 2, EU_MUL, 2, 1, RVF_OVERLAP_DST_SRC);
223    t[RV_KIND_MULW as usize] = rgc_ov(4, 2, 3, EU_MUL, 2, 1, RVF_OVERLAP_DST_SRC);
224    t[RV_KIND_MULH as usize] = rgc(4, 4, EU_MUL, 2, 1, 0);
225    t[RV_KIND_MULHSU as usize] = rgc(6, 4, EU_MUL, 2, 1, 0);
226    // Divides
227    t[RV_KIND_DIV as usize] = rgc(60, 4, EU_DIV, 2, 1, 0);
228    // Zbb 1-cycle unary
229    t[RV_KIND_ZBB_U1 as usize] = rgc(1, 1, EU_ALU, 1, 1, 0);
230    // Zbb 2-cycle ctz
231    t[RV_KIND_ZBB_CTZ as usize] = rgc(2, 1, EU_ALU, 1, 1, 0);
232    // Zbb min/max (overlap rule)
233    t[RV_KIND_ZBB_MINMAX as usize] = rgc_ov(3, 2, 3, EU_ALU, 2, 1, RVF_OVERLAP_DST_SRC);
234    // Zbb inverted bitwise (no overlap)
235    t[RV_KIND_ZBB_INV as usize] = rgc(2, 3, EU_ALU, 2, 1, 0);
236    // Zbb xnor (overlap rule)
237    t[RV_KIND_ZBB_XNOR as usize] = rgc_ov(2, 2, 3, EU_ALU, 2, 1, RVF_OVERLAP_DST_SRC);
238    // Zbb rotates (rs1 == rd rule)
239    t[RV_KIND_ZBB_ROT as usize] = rgc_ov(1, 2, 3, EU_ALU, 2, 1, RVF_OVERLAP_RS1_RD);
240    t[RV_KIND_ZBB_RORI as usize] = rgc_ov(1, 1, 2, EU_ALU, 1, 1, RVF_OVERLAP_DST_SRC);
241    t[RV_KIND_ZBB_ROTW as usize] = rgc_ov(2, 3, 4, EU_ALU, 2, 1, RVF_OVERLAP_RS1_RD);
242    t[RV_KIND_ZBB_RORIW as usize] = rgc_ov(2, 2, 3, EU_ALU, 1, 1, RVF_OVERLAP_DST_SRC);
243    // Zba shift-add
244    t[RV_KIND_ZBA as usize] = rgc_ov(1, 1, 2, EU_ALU, 2, 1, RVF_OVERLAP_DST_SRC);
245    t[RV_KIND_ZBA_IMM as usize] = rgc_ov(1, 1, 2, EU_ALU, 1, 1, RVF_OVERLAP_DST_SRC);
246    // Zbs single-bit
247    t[RV_KIND_ZBS as usize] = rgc_ov(1, 1, 2, EU_ALU, 2, 1, RVF_OVERLAP_DST_SRC);
248    t[RV_KIND_ZBS_IMM as usize] = rgc_ov(1, 1, 2, EU_ALU, 1, 1, RVF_OVERLAP_DST_SRC);
249    // Zicond
250    t[RV_KIND_ZICOND as usize] = rgc(2, 2, EU_ALU, 2, 1, 0);
251    t
252};
253
254/// PVM2 register → simulator slot (u8). 0xFF means "no register" — the
255/// simulator's `feed_direct` interprets that as "skip dep / no write".
256/// Single source: [`crate::regs::reg_slot_or_ff`] (the recompiler reads
257/// the same classification via `REG_SLOT_LUT`, so gas agrees bit-for-bit).
258#[inline(always)]
259pub fn rv_slot_u8(r: u8) -> u8 {
260    crate::regs::reg_slot_or_ff(r)
261}
262
263/// True iff a simulator slot is a *spilled* register (`x3`/`x4` → slots
264/// 13/14). Used to charge the memory-spill gas cost. `0xFF` ("no register")
265/// is `255`, so it is correctly excluded.
266#[inline(always)]
267fn is_spilled_slot(slot: u8) -> bool {
268    slot == 13 || slot == 14
269}
270
271/// Compute the [`crate::predecode::RvGasMeta`] for an `Inst`.
272/// Called once per instruction at decode time; the result is cached
273/// in [`crate::predecode::RvPreDecodedInst::gas_meta`] so the gas
274/// hot path doesn't have to re-match the variant.
275#[inline]
276pub fn rv_gas_meta(inst: &crate::instruction::Inst) -> crate::predecode::RvGasMeta {
277    let (kind, rs1, rs2, rd) = rv_op_metadata(inst);
278    let entry = &RV_GAS_COST_LUT[kind as usize];
279    // Pre-mask the register fields per the LUT's reg patterns so the
280    // hot path doesn't have to consult `src_pat` / `dst_pat`.
281    let src1_slot = if entry.src_pat >= 1 {
282        rv_slot_u8(rs1)
283    } else {
284        0xFF
285    };
286    let src2_slot = if entry.src_pat == 2 {
287        rv_slot_u8(rs2)
288    } else {
289        0xFF
290    };
291    let dst_slot = if entry.dst_pat == 1 {
292        rv_slot_u8(rd)
293    } else {
294        0xFF
295    };
296    crate::predecode::RvGasMeta {
297        kind,
298        src1_slot,
299        src2_slot,
300        dst_slot,
301    }
302}
303
304/// Single match over `Inst` returning `(kind, rs1, rs2, rd)`. Each
305/// field is u8; the tuple packs into a u32 register so the call site
306/// is cheap. Variants that share a gas cost share a kind.
307#[inline(always)]
308fn rv_op_metadata(inst: &crate::instruction::Inst) -> (u8, u8, u8, u8) {
309    use crate::instruction::Inst::*;
310    match *inst {
311        // No-reg / no-arg
312        Trap => (RV_KIND_TRAP, 0, 0, 0),
313        Fallthrough => (RV_KIND_FALLTHROUGH, 0, 0, 0),
314        EcallJar => (RV_KIND_ECALL_JAR, 0, 0, 0),
315        Ecalli { .. } => (RV_KIND_ECALLI, 0, 0, 0),
316        Fence | FenceI => (RV_KIND_FENCE, 0, 0, 0),
317        Reserved { .. } => (RV_KIND_RESERVED, 0, 0, 0),
318
319        // JAL — terminator with link
320        Jal { rd, .. } => (RV_KIND_JAL, 0, 0, rd),
321
322        // JALR — indirect-jump terminator. src = rs1 (target); rd
323        // (return addr) isn't tracked (terminator, no in-block successor).
324        Jalr { rs1, .. } => (RV_KIND_JALR, rs1, 0, 0),
325
326        // Branches
327        Beq { rs1, rs2, .. }
328        | Bne { rs1, rs2, .. }
329        | Blt { rs1, rs2, .. }
330        | Bge { rs1, rs2, .. }
331        | Bltu { rs1, rs2, .. }
332        | Bgeu { rs1, rs2, .. } => (RV_KIND_BRANCH, rs1, rs2, 0),
333
334        // Loads
335        Lb { rd, rs1, .. }
336        | Lh { rd, rs1, .. }
337        | Lw { rd, rs1, .. }
338        | Ld { rd, rs1, .. }
339        | Lbu { rd, rs1, .. }
340        | Lhu { rd, rs1, .. }
341        | Lwu { rd, rs1, .. } => (RV_KIND_LOAD, rs1, 0, rd),
342
343        // Stores
344        Sb { rs1, rs2, .. } | Sh { rs1, rs2, .. } | Sw { rs1, rs2, .. } | Sd { rs1, rs2, .. } => {
345            (RV_KIND_STORE, rs1, rs2, 0)
346        }
347
348        // Upper immediate; auipc folds to a constant materialise (== lui cost).
349        Lui { rd, .. } => (RV_KIND_LUI, 0, 0, rd),
350        Auipc { rd, .. } => (RV_KIND_LUI, 0, 0, rd),
351
352        // 64-bit I-type ALU
353        Addi { rd, rs1, .. }
354        | Andi { rd, rs1, .. }
355        | Ori { rd, rs1, .. }
356        | Xori { rd, rs1, .. }
357        | Sltiu { rd, rs1, .. }
358        | Slti { rd, rs1, .. }
359        | Slli { rd, rs1, .. }
360        | Srli { rd, rs1, .. }
361        | Srai { rd, rs1, .. } => (RV_KIND_ADDI, rs1, 0, rd),
362
363        // 32-bit I-type ALU
364        Addiw { rd, rs1, .. }
365        | Slliw { rd, rs1, .. }
366        | Srliw { rd, rs1, .. }
367        | Sraiw { rd, rs1, .. } => (RV_KIND_ADDIW, rs1, 0, rd),
368
369        // 64-bit R-type ALU
370        Add { rd, rs1, rs2 }
371        | Sub { rd, rs1, rs2 }
372        | And { rd, rs1, rs2 }
373        | Or { rd, rs1, rs2 }
374        | Xor { rd, rs1, rs2 } => (RV_KIND_ADD, rs1, rs2, rd),
375        // 64-bit shifts
376        Sll { rd, rs1, rs2 } | Srl { rd, rs1, rs2 } | Sra { rd, rs1, rs2 } => {
377            (RV_KIND_SLL, rs1, rs2, rd)
378        }
379        // 64-bit compare
380        Slt { rd, rs1, rs2 } | Sltu { rd, rs1, rs2 } => (RV_KIND_SLT, rs1, rs2, rd),
381
382        // 32-bit R-type ALU
383        Addw { rd, rs1, rs2 } | Subw { rd, rs1, rs2 } => (RV_KIND_ADDW, rs1, rs2, rd),
384        // 32-bit shifts
385        Sllw { rd, rs1, rs2 } | Srlw { rd, rs1, rs2 } | Sraw { rd, rs1, rs2 } => {
386            (RV_KIND_SLLW, rs1, rs2, rd)
387        }
388
389        // Multiplies
390        Mul { rd, rs1, rs2 } => (RV_KIND_MUL, rs1, rs2, rd),
391        Mulw { rd, rs1, rs2 } => (RV_KIND_MULW, rs1, rs2, rd),
392        Mulh { rd, rs1, rs2 } | Mulhu { rd, rs1, rs2 } => (RV_KIND_MULH, rs1, rs2, rd),
393        Mulhsu { rd, rs1, rs2 } => (RV_KIND_MULHSU, rs1, rs2, rd),
394
395        // Divides
396        Div { rd, rs1, rs2 }
397        | Divu { rd, rs1, rs2 }
398        | Rem { rd, rs1, rs2 }
399        | Remu { rd, rs1, rs2 }
400        | Divw { rd, rs1, rs2 }
401        | Divuw { rd, rs1, rs2 }
402        | Remw { rd, rs1, rs2 }
403        | Remuw { rd, rs1, rs2 } => (RV_KIND_DIV, rs1, rs2, rd),
404
405        // Zbb 1-cycle unary
406        Clz { rd, rs1 }
407        | Clzw { rd, rs1 }
408        | Cpop { rd, rs1 }
409        | Cpopw { rd, rs1 }
410        | SextB { rd, rs1 }
411        | SextH { rd, rs1 }
412        | ZextH { rd, rs1 }
413        | Rev8 { rd, rs1 }
414        | OrcB { rd, rs1 } => (RV_KIND_ZBB_U1, rs1, 0, rd),
415        // Zbb 2-cycle
416        Ctz { rd, rs1 } | Ctzw { rd, rs1 } => (RV_KIND_ZBB_CTZ, rs1, 0, rd),
417        // Zbb min/max
418        Min { rd, rs1, rs2 }
419        | Minu { rd, rs1, rs2 }
420        | Max { rd, rs1, rs2 }
421        | Maxu { rd, rs1, rs2 } => (RV_KIND_ZBB_MINMAX, rs1, rs2, rd),
422        // Zbb inv-bitwise
423        Andn { rd, rs1, rs2 } | Orn { rd, rs1, rs2 } => (RV_KIND_ZBB_INV, rs1, rs2, rd),
424        Xnor { rd, rs1, rs2 } => (RV_KIND_ZBB_XNOR, rs1, rs2, rd),
425        // Zbb rotates
426        Rol { rd, rs1, rs2 } | Ror { rd, rs1, rs2 } => (RV_KIND_ZBB_ROT, rs1, rs2, rd),
427        Rori { rd, rs1, .. } => (RV_KIND_ZBB_RORI, rs1, 0, rd),
428        Rolw { rd, rs1, rs2 } | Rorw { rd, rs1, rs2 } => (RV_KIND_ZBB_ROTW, rs1, rs2, rd),
429        Roriw { rd, rs1, .. } => (RV_KIND_ZBB_RORIW, rs1, 0, rd),
430
431        // Zba
432        Sh1add { rd, rs1, rs2 }
433        | Sh2add { rd, rs1, rs2 }
434        | Sh3add { rd, rs1, rs2 }
435        | Sh1adduw { rd, rs1, rs2 }
436        | Sh2adduw { rd, rs1, rs2 }
437        | Sh3adduw { rd, rs1, rs2 }
438        | Adduw { rd, rs1, rs2 } => (RV_KIND_ZBA, rs1, rs2, rd),
439        Slliuw { rd, rs1, .. } => (RV_KIND_ZBA_IMM, rs1, 0, rd),
440
441        // Zbs
442        Bclr { rd, rs1, rs2 }
443        | Bset { rd, rs1, rs2 }
444        | Binv { rd, rs1, rs2 }
445        | Bext { rd, rs1, rs2 } => (RV_KIND_ZBS, rs1, rs2, rd),
446        Bclri { rd, rs1, .. }
447        | Bseti { rd, rs1, .. }
448        | Binvi { rd, rs1, .. }
449        | Bexti { rd, rs1, .. } => (RV_KIND_ZBS_IMM, rs1, 0, rd),
450
451        // Zicond
452        CzeroEqz { rd, rs1, rs2 } | CzeroNez { rd, rs1, rs2 } => (RV_KIND_ZICOND, rs1, rs2, rd),
453    }
454}
455
456/// Kind-driven PVM2 gas feed: look up the cost LUT, compute the
457/// overlap-dependent decode_slots and the mem_cycles override, then
458/// feed the simulator via `feed_direct`. Takes raw `(kind, src1,
459/// src2, dst)` so the recompiler's per-arm dispatch can supply them
460/// as compile-time constants + slot lookups without going through
461/// an `RvGasMeta` struct.
462///
463/// Slots are PVM2 register indices (0..14) or `0xFF` for "no
464/// register" (x0 / absent operand). Slots 13/14 are `x3`/`x4`, which are
465/// host-spilled — see the spill-cost charge below.
466///
467/// Returns `is_terminator` (RVF_TERM flag from the LUT entry).
468#[inline(always)]
469pub fn rv_feed_gas_kind(
470    kind: u8,
471    src1: u8,
472    src2: u8,
473    dst: u8,
474    gas_sim: &mut crate::gas_sim::GasSimulator,
475    mem_cycles: u8,
476) -> bool {
477    let entry = &RV_GAS_COST_LUT[kind as usize];
478
479    // mem_cycles override for LOAD/STORE rows.
480    let cycles = if entry.exec_unit == EU_LOAD || entry.exec_unit == EU_STORE {
481        mem_cycles
482    } else {
483        entry.cycles
484    };
485
486    // x3/x4 (slots 13/14) are host-spilled: a host with exactly 13 registers
487    // materialises them from memory on each access. Charge the memory-spill
488    // cost (`mem_cycles`) per spilled operand position, on top of the base
489    // cycles — matching the load/op/store the recompiler's spill path emits,
490    // and bounding the worst-case conforming host. Conformant code never
491    // names x3/x4 (every slot is < 13), so this adds exactly nothing there.
492    let spill_ops =
493        is_spilled_slot(src1) as u8 + is_spilled_slot(src2) as u8 + is_spilled_slot(dst) as u8;
494    let cycles = cycles.saturating_add(mem_cycles.saturating_mul(spill_ops));
495
496    // Overlap-dependent decode_slots.
497    let decode_slots = if entry.flags & RVF_OVERLAP_DST_SRC != 0 {
498        // Overlap holds when dst (non-empty) matches any active source.
499        let overlap = dst != 0xFF && (dst == src1 || dst == src2);
500        if overlap {
501            entry.overlap_slots & 0x0F
502        } else {
503            entry.overlap_slots >> 4
504        }
505    } else if entry.flags & RVF_OVERLAP_RS1_RD != 0 {
506        // Shifts: rs1 == rd in RV ⇔ src1_slot == dst_slot post-mapping.
507        let overlap = dst != 0xFF && src1 != 0xFF && dst == src1;
508        if overlap {
509            entry.overlap_slots & 0x0F
510        } else {
511            entry.overlap_slots >> 4
512        }
513    } else {
514        entry.decode_slots
515    };
516
517    gas_sim.feed_direct(cycles, decode_slots, src1, src2, dst);
518    entry.flags & RVF_TERM != 0
519}
520
521/// Predecode-cached variant: same logic as [`rv_feed_gas_kind`] but
522/// takes a pre-resolved [`crate::predecode::RvGasMeta`]. Used by
523/// the per-block gas-cost helper that consumes the `Predecode`
524/// vector built by `predecode`.
525///
526/// Returns `is_terminator`.
527#[inline(always)]
528pub fn rv_feed_gas_direct(
529    meta: &crate::predecode::RvGasMeta,
530    gas_sim: &mut crate::gas_sim::GasSimulator,
531    mem_cycles: u8,
532) -> bool {
533    rv_feed_gas_kind(
534        meta.kind,
535        meta.src1_slot,
536        meta.src2_slot,
537        meta.dst_slot,
538        gas_sim,
539        mem_cycles,
540    )
541}
542
543/// Compute per-block gas cost for a PVM2 basic block. The block runs
544/// from `insts[block_start]` until the next instruction whose
545/// `is_gas_block_start` is true (or the end of `insts`). Uses the
546/// single-pass `GasSimulator` (decode-throughput + register-readiness
547/// tracking — the model from `spec/Jar/JAVM/GasCostSinglePass.lean`)
548/// driven by the LUT fast path (`rv_feed_gas_direct`).
549/// Returns `max(max_done − 3, 1)`.
550pub fn rv_gas_cost_for_block(
551    insts: &[crate::predecode::RvPreDecodedInst],
552    block_start: usize,
553    mem_cycles: u8,
554) -> u32 {
555    let mut end = block_start + 1;
556    while end < insts.len() && !insts[end].is_gas_block_start {
557        end += 1;
558    }
559    let mut sim = crate::gas_sim::GasSimulator::new();
560    for i in &insts[block_start..end] {
561        rv_feed_gas_direct(&i.gas_meta, &mut sim, mem_cycles);
562    }
563    sim.flush_and_get_cost()
564}
565
566/// Memory class of a gas `kind` for category-#3 reserve accounting.
567#[derive(Clone, Copy, Debug, PartialEq, Eq)]
568pub enum MemClass {
569    None,
570    Load,
571    Store,
572}
573
574/// Classify a gas `kind` as a category-#3 memory op. Uses the **same**
575/// `exec_unit == EU_LOAD/EU_STORE` predicate `rv_feed_gas_kind` uses for
576/// the #2 `mem_cycles` override (gas_cost.rs), so "memory op for #3" and
577/// "memory op for #2" provably agree across both engines.
578#[inline]
579pub fn rv_kind_mem_class(kind: u8) -> MemClass {
580    match RV_GAS_COST_LUT[kind as usize].exec_unit {
581        EU_LOAD => MemClass::Load,
582        EU_STORE => MemClass::Store,
583        _ => MemClass::None,
584    }
585}
586
587/// Per-instruction worst-case category-#3 reserve contribution for a
588/// gas `kind`: `store → COW×2` (×2 = `MAX_PAGES_PER_ACCESS`), else 0.
589/// **Loads no longer reserve** — read-only page-in is charged eagerly at
590/// the CALL (`gas_const::call_frame_cost`), not at a fault, so the only
591/// #3 a block can debit mid-flight is a store's copy-on-write. **Both
592/// engines** accumulate this at every real gas feed, so the block reserve
593/// matches bit-for-bit.
594#[inline]
595pub fn rv_kind_reserve(kind: u8) -> u32 {
596    use crate::gas_const::{COW_COST, MAX_PAGES_PER_ACCESS};
597    let r = match rv_kind_mem_class(kind) {
598        MemClass::Load => 0,
599        MemClass::Store => COW_COST.saturating_mul(MAX_PAGES_PER_ACCESS),
600        MemClass::None => 0,
601    };
602    r.min(u32::MAX as u64) as u32
603}
604
605/// Worst-case category-#3 reserve (in gas) for the basic block starting
606/// at `block_start`: the maximum materialization cost the block can
607/// debit at faults. The block-entry gate reserves this (a gate, not a
608/// charge — see `~/docs/spec-staging/gas-cost.md` §1/§3) so #3 can never
609/// drive gas negative mid-block.
610///
611/// Sums [`rv_kind_reserve`] over the block. An `ecall`/`ecalli` block
612/// contributes 0 — it has no load/store, and its #3 (the dynamic
613/// call-frame / host cost) is charged separately at dispatch. Saturates
614/// to `u32::MAX` (blocks are bounded by code size, so this never binds).
615pub fn rv_block_reserve_for_block(
616    insts: &[crate::predecode::RvPreDecodedInst],
617    block_start: usize,
618) -> u32 {
619    let mut end = block_start + 1;
620    while end < insts.len() && !insts[end].is_gas_block_start {
621        end += 1;
622    }
623    let mut reserve: u64 = 0;
624    for i in &insts[block_start..end] {
625        reserve = reserve.saturating_add(rv_kind_reserve(i.gas_meta.kind) as u64);
626    }
627    reserve.min(u32::MAX as u64) as u32
628}
629
630#[cfg(test)]
631mod tests {
632    use super::*;
633    use crate::instruction::decode;
634    use crate::predecode::is_terminator;
635
636    /// Terminator flag as the recompiler sees it: the gas-LUT `RVF_TERM`
637    /// bit for the instruction's kind (this is the classifier
638    /// `Compiler::feed_gas_rv` consumes to mark gas-block starts).
639    fn lut_term(inst: &crate::instruction::Inst) -> bool {
640        RV_GAS_COST_LUT[rv_gas_meta(inst).kind as usize].flags & RVF_TERM != 0
641    }
642
643    /// B5: "what is a terminator" is hand-coded twice in this crate —
644    /// `predecode::is_terminator` (a `match` on `Inst`, which the
645    /// interpreter uses to mark gas-block starts) and the gas-LUT
646    /// `RVF_TERM` bit (which the recompiler uses for the same purpose).
647    /// They are independent and must classify every decodable
648    /// instruction identically, or the interpreter and recompiler would
649    /// disagree on basic-block boundaries (a consensus fork). Sweep the
650    /// whole RVC halfword space and a 4-byte opcode × funct matrix and
651    /// assert the two classifiers agree on every encoding that decodes.
652    #[test]
653    fn predecode_and_gas_lut_terminator_agree() {
654        // RVC: every 2-byte halfword whose low bits aren't the 4-byte
655        // marker.
656        for h in 0u16..=0xFFFF {
657            if h & 0b11 == 0b11 {
658                continue;
659            }
660            if let Some((inst, _len)) = decode(&h.to_le_bytes()) {
661                assert_eq!(
662                    is_terminator(&inst),
663                    lut_term(&inst),
664                    "RVC halfword {h:#06x} -> {inst:?}"
665                );
666            }
667        }
668        // 4-byte: sweep every 7-bit opcode (low bits = 0b11) across
669        // funct3 and a representative set of funct7 values, with fixed
670        // register fields. This reaches every terminator opcode (jal
671        // 0x6F, jalr 0x67, branch 0x63, custom-0 0x0B across all funct3)
672        // and every non-terminator class.
673        for major in 0u32..0x20 {
674            let opcode7 = (major << 2) | 0b11;
675            for funct3 in 0u32..8 {
676                for &funct7 in &[0u32, 0b000_0001, 0b010_0000, 0b000_0111, 0b011_0000] {
677                    let w = (funct7 << 25)
678                        | (11 << 20)
679                        | (10 << 15)
680                        | (funct3 << 12)
681                        | (10 << 7)
682                        | opcode7;
683                    if let Some((inst, _len)) = decode(&w.to_le_bytes()) {
684                        assert_eq!(
685                            is_terminator(&inst),
686                            lut_term(&inst),
687                            "word {w:#010x} -> {inst:?}"
688                        );
689                    }
690                }
691            }
692        }
693    }
694}