javm_exec/gas_sim.rs
1//! Single-pass pipeline gas model (JAR v0.8.0).
2//!
3//! O(n) single-pass model tracking per-register completion cycles.
4//! Replaces the full ROB-based pipeline simulation.
5//!
6//! Tracks `reg_done[15]` (cycle when each register is ready) and decode
7//! throughput (4 slots/cycle). No ROB, no priority loop, no EU contention.
8//! See `docs/gas-metering-design.md` for detailed comparison.
9
10use crate::gas_cost::FastCost;
11
12/// Single-pass pipeline gas simulator. O(1) per instruction, stack-allocated.
13pub struct GasSimulator {
14 reg_done: [u32; 15],
15 cycle: u32,
16 decode_used: u8,
17 max_done: u32,
18}
19
20impl Default for GasSimulator {
21 fn default() -> Self {
22 Self::new()
23 }
24}
25
26impl GasSimulator {
27 pub fn new() -> Self {
28 Self {
29 reg_done: [0; 15],
30 cycle: 0,
31 decode_used: 0,
32 max_done: 0,
33 }
34 }
35
36 /// Fast path: feed an instruction using direct register indices instead of
37 /// bitmasks. Avoids the shift+OR bitmask construction and trailing_zeros
38 /// extraction loop. For typical 2-source, 1-dest instructions.
39 /// `src1`/`src2` are source register indices (0..14, or 0xFF for "none").
40 /// `dst` is destination register index (0..14, or 0xFF for "none").
41 #[inline(always)]
42 pub fn feed_direct(&mut self, cycles: u8, decode_slots: u8, src1: u8, src2: u8, dst: u8) {
43 // Match Lean semantics: advance cycle only if ALL 4 decode slots are
44 // already consumed. As long as ≥1 slot remains, the new instruction
45 // begins decoding this cycle regardless of how many slots it needs.
46 if self.decode_used >= 4 {
47 self.cycle += 1;
48 self.decode_used = decode_slots;
49 } else {
50 self.decode_used += decode_slots;
51 }
52 let mut start = self.cycle;
53 if src1 < 15 {
54 start = start.max(self.reg_done[src1 as usize]);
55 }
56 if src2 < 15 {
57 start = start.max(self.reg_done[src2 as usize]);
58 }
59 let done = start + cycles as u32;
60 if dst < 15 {
61 self.reg_done[dst as usize] = done;
62 }
63 self.max_done = self.max_done.max(done);
64 }
65
66 /// Process one instruction. O(1).
67 #[inline]
68 pub fn feed(&mut self, cost: &FastCost) {
69 // Decode throughput: 4 slots per cycle.
70 // Match Lean semantics: advance cycle only if ALL 4 slots consumed.
71 if self.decode_used >= 4 {
72 self.cycle += 1;
73 self.decode_used = cost.decode_slots;
74 } else {
75 self.decode_used += cost.decode_slots;
76 }
77
78 // move_reg: zero-cycle frontend-only op, propagate reg_done
79 if cost.is_move_reg {
80 let src_reg = cost.src_mask.trailing_zeros() as usize;
81 let dst_reg = cost.dst_mask.trailing_zeros() as usize;
82 if src_reg < 15 && dst_reg < 15 {
83 self.reg_done[dst_reg] = self.reg_done[src_reg];
84 }
85 return;
86 }
87
88 // Data dependencies: start = max(decode_cycle, max(reg_done[src_regs]))
89 let mut start = self.cycle;
90 let mut src = cost.src_mask;
91 while src != 0 {
92 let r = src.trailing_zeros() as usize;
93 src &= src - 1;
94 if r < 15 {
95 start = start.max(self.reg_done[r]);
96 }
97 }
98
99 // Completion
100 let done = start + cost.cycles as u32;
101
102 // Update destination registers
103 let mut dst = cost.dst_mask;
104 while dst != 0 {
105 let r = dst.trailing_zeros() as usize;
106 dst &= dst - 1;
107 if r < 15 {
108 self.reg_done[r] = done;
109 }
110 }
111
112 // Track maximum completion cycle
113 self.max_done = self.max_done.max(done);
114 }
115
116 /// Return block gas cost: max(max_done - 3, 1).
117 #[inline]
118 pub fn flush_and_get_cost(&self) -> u32 {
119 if self.max_done > 3 {
120 self.max_done - 3
121 } else {
122 1
123 }
124 }
125
126 /// Reset for the next gas block.
127 #[inline]
128 pub fn reset(&mut self) {
129 self.reg_done = [0; 15];
130 self.cycle = 0;
131 self.decode_used = 0;
132 self.max_done = 0;
133 }
134}