Skip to main content

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[13]` (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; 13],
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; 13],
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..12, or 0xFF for "none").
40    /// `dst` is destination register index (0..12, 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 < 13 {
54            start = start.max(self.reg_done[src1 as usize]);
55        }
56        if src2 < 13 {
57            start = start.max(self.reg_done[src2 as usize]);
58        }
59        let done = start + cycles as u32;
60        if dst < 13 {
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 < 13 && dst_reg < 13 {
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 < 13 {
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 < 13 {
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; 13];
130        self.cycle = 0;
131        self.decode_used = 0;
132        self.max_done = 0;
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    // === flush_and_get_cost ===
141
142    #[test]
143    fn test_empty_block_cost_is_one() {
144        let sim = GasSimulator::new();
145        assert_eq!(sim.flush_and_get_cost(), 1, "empty block should cost 1");
146    }
147
148    // === feed_direct ===
149
150    #[test]
151    fn test_single_alu_instruction() {
152        // One ALU op: 1 cycle, 1 decode slot, r0 → r2
153        let mut sim = GasSimulator::new();
154        sim.feed_direct(1, 1, 0, 0xFF, 2); // src1=r0, no src2, dst=r2
155        // max_done = 0 (start) + 1 (cycles) = 1
156        // cost = max(1 - 3, 1) = 1
157        assert_eq!(sim.flush_and_get_cost(), 1);
158    }
159
160    #[test]
161    fn test_data_dependency_chain() {
162        // Chain: r0 → r1 (1 cycle), r1 → r2 (1 cycle)
163        // r1 is ready at cycle 1, r2 at cycle 2
164        let mut sim = GasSimulator::new();
165        sim.feed_direct(1, 1, 0, 0xFF, 1); // r1 done at cycle 1
166        sim.feed_direct(1, 1, 1, 0xFF, 2); // depends on r1, r2 done at cycle 2
167        // max_done = 2, cost = max(2 - 3, 1) = 1
168        assert_eq!(sim.flush_and_get_cost(), 1);
169    }
170
171    #[test]
172    fn test_long_dependency_chain() {
173        // 5-deep chain, each 1 cycle: r0→r1→r2→r3→r4→r5
174        let mut sim = GasSimulator::new();
175        for i in 0..5u8 {
176            sim.feed_direct(1, 1, i, 0xFF, i + 1);
177        }
178        // r5 done at cycle 5, cost = max(5 - 3, 1) = 2
179        assert_eq!(sim.flush_and_get_cost(), 2);
180    }
181
182    #[test]
183    fn test_independent_instructions_parallel() {
184        // Two independent ALU ops: r0→r2 and r1→r3, both 1 cycle
185        let mut sim = GasSimulator::new();
186        sim.feed_direct(1, 1, 0, 0xFF, 2);
187        sim.feed_direct(1, 1, 1, 0xFF, 3);
188        // Both start at cycle 0, done at cycle 1
189        // max_done = 1, cost = 1
190        assert_eq!(sim.flush_and_get_cost(), 1);
191    }
192
193    #[test]
194    fn test_multi_cycle_instruction() {
195        // One 4-cycle instruction (e.g., multiply)
196        let mut sim = GasSimulator::new();
197        sim.feed_direct(4, 1, 0, 1, 2); // 4 cycles, src r0+r1, dst r2
198        // max_done = 4, cost = max(4 - 3, 1) = 1
199        assert_eq!(sim.flush_and_get_cost(), 1);
200    }
201
202    #[test]
203    fn test_high_latency_chain() {
204        // 4-cycle MUL → 1-cycle ALU dependent on result
205        let mut sim = GasSimulator::new();
206        sim.feed_direct(4, 1, 0, 1, 2); // MUL: r2 done at cycle 4
207        sim.feed_direct(1, 1, 2, 0xFF, 3); // ALU: depends on r2, r3 done at cycle 5
208        // max_done = 5, cost = max(5 - 3, 1) = 2
209        assert_eq!(sim.flush_and_get_cost(), 2);
210    }
211
212    #[test]
213    fn test_decode_throughput_limit() {
214        // 5 independent 1-slot instructions: 4 fit in cycle 0, 5th bumps to cycle 1
215        let mut sim = GasSimulator::new();
216        for i in 0..5u8 {
217            sim.feed_direct(1, 1, 0xFF, 0xFF, i); // no deps, 1 slot each
218        }
219        // First 4 decode in cycle 0 (done at 1), 5th decodes in cycle 1 (done at 2)
220        // max_done = 2, cost = max(2 - 3, 1) = 1
221        assert_eq!(sim.flush_and_get_cost(), 1);
222    }
223
224    #[test]
225    fn test_no_src_no_dst() {
226        // Instruction with no register deps (e.g., NOP-like)
227        let mut sim = GasSimulator::new();
228        sim.feed_direct(1, 1, 0xFF, 0xFF, 0xFF);
229        // max_done = 1 (start 0 + 1 cycle)
230        assert_eq!(sim.flush_and_get_cost(), 1);
231    }
232
233    #[test]
234    fn test_two_sources() {
235        // r2 = r0 + r1 where r0 available at cycle 0, r1 available at cycle 3
236        let mut sim = GasSimulator::new();
237        sim.feed_direct(3, 1, 0xFF, 0xFF, 1); // r1 done at cycle 3
238        sim.feed_direct(1, 1, 0, 1, 2); // depends on r0 (ready 0) and r1 (ready 3)
239        // r2 starts at max(0, 3) = 3, done at 4
240        // max_done = 4, cost = max(4 - 3, 1) = 1
241        assert_eq!(sim.flush_and_get_cost(), 1);
242    }
243
244    // === feed (bitmask-based) ===
245
246    #[test]
247    fn test_feed_move_reg_propagates_done() {
248        // move_reg: zero-cycle, propagates reg_done from src to dst
249        let mut sim = GasSimulator::new();
250        sim.feed_direct(3, 1, 0xFF, 0xFF, 0); // r0 done at cycle 3
251        sim.feed(&FastCost {
252            cycles: 0,
253            decode_slots: 1,
254            exec_unit: 0,
255            src_mask: 1 << 0, // r0
256            dst_mask: 1 << 1, // r1
257            is_terminator: false,
258            is_move_reg: true,
259        });
260        // r1 should inherit r0's done time (3)
261        sim.feed_direct(1, 1, 1, 0xFF, 2); // depends on r1
262        // r2 starts at 3, done at 4
263        // max_done = 4, cost = max(4 - 3, 1) = 1
264        assert_eq!(sim.flush_and_get_cost(), 1);
265    }
266
267    #[test]
268    fn test_feed_bitmask_multiple_sources() {
269        let mut sim = GasSimulator::new();
270        sim.feed_direct(2, 1, 0xFF, 0xFF, 0); // r0 done at 2
271        sim.feed_direct(3, 1, 0xFF, 0xFF, 1); // r1 done at 3
272        sim.feed(&FastCost {
273            cycles: 1,
274            decode_slots: 1,
275            exec_unit: 1,                  // ALU
276            src_mask: (1 << 0) | (1 << 1), // r0 + r1
277            dst_mask: 1 << 2,              // r2
278            is_terminator: false,
279            is_move_reg: false,
280        });
281        // r2 starts at max(2, 3) = 3, done at 4
282        // max_done = 4, cost = max(4 - 3, 1) = 1
283        assert_eq!(sim.flush_and_get_cost(), 1);
284    }
285
286    // === reset ===
287
288    #[test]
289    fn test_reset_clears_state() {
290        let mut sim = GasSimulator::new();
291        sim.feed_direct(10, 1, 0xFF, 0xFF, 0); // large cost
292        assert!(sim.flush_and_get_cost() > 1);
293        sim.reset();
294        assert_eq!(sim.flush_and_get_cost(), 1, "after reset, cost should be 1");
295    }
296}
297
298#[cfg(test)]
299mod proptests {
300    use super::*;
301    use proptest::prelude::*;
302
303    proptest! {
304        /// flush_and_get_cost always returns at least 1.
305        #[test]
306        fn cost_always_at_least_one(
307            instrs in proptest::collection::vec(
308                (1u8..20, 1u8..4, 0u8..13, 0u8..13),
309                0..10,
310            ),
311        ) {
312            let mut sim = GasSimulator::new();
313            for (cycles, slots, src, dst) in &instrs {
314                sim.feed_direct(*cycles, *slots, *src, 0xFF, *dst);
315            }
316            prop_assert!(sim.flush_and_get_cost() >= 1);
317        }
318
319        /// reset returns the simulator to the empty state (cost = 1).
320        #[test]
321        fn reset_restores_empty_state(
322            instrs in proptest::collection::vec(
323                (1u8..20, 1u8..4, 0u8..13, 0u8..13),
324                1..10,
325            ),
326        ) {
327            let mut sim = GasSimulator::new();
328            for (cycles, slots, src, dst) in &instrs {
329                sim.feed_direct(*cycles, *slots, *src, 0xFF, *dst);
330            }
331            sim.reset();
332            prop_assert_eq!(sim.flush_and_get_cost(), 1);
333        }
334
335        /// Independent instructions (no register deps) never cost more than
336        /// a dependency chain of the same length would.
337        #[test]
338        fn independent_no_more_than_chained(
339            count in 1usize..8,
340            cycles in 1u8..10,
341        ) {
342            // Independent: all use different dst, no src deps
343            let mut indep = GasSimulator::new();
344            for i in 0..count.min(13) {
345                indep.feed_direct(cycles, 1, 0xFF, 0xFF, i as u8);
346            }
347            // Chained: r0 -> r1 -> r2 -> ...
348            let mut chain = GasSimulator::new();
349            for i in 0..count.min(12) {
350                chain.feed_direct(cycles, 1, i as u8, 0xFF, (i + 1) as u8);
351            }
352            prop_assert!(indep.flush_and_get_cost() <= chain.flush_and_get_cost());
353        }
354
355        /// feed_direct with no sources and no dest (0xFF) is equivalent to
356        /// a no-dep instruction — cost grows only from decode throughput.
357        #[test]
358        fn no_reg_deps_bounded_by_decode(
359            count in 1usize..20,
360            cycles in 1u8..5,
361        ) {
362            let mut sim = GasSimulator::new();
363            for _ in 0..count {
364                sim.feed_direct(cycles, 1, 0xFF, 0xFF, 0xFF);
365            }
366            // max_done = (cycle_when_last_decoded) + cycles
367            // With 4 decode slots/cycle, last decode is at cycle floor((count-1)/4)
368            // So max_done <= floor((count-1)/4) + cycles
369            let expected_max = ((count - 1) / 4) as u32 + cycles as u32;
370            let cost = sim.flush_and_get_cost();
371            let expected_cost = if expected_max > 3 { expected_max - 3 } else { 1 };
372            prop_assert_eq!(cost, expected_cost);
373        }
374
375        /// feed and feed_direct produce the same cost for single-source,
376        /// single-dest instructions.
377        #[test]
378        fn feed_matches_feed_direct(
379            cycles in 1u8..20,
380            decode_slots in 1u8..4,
381            src in 0u8..13,
382            dst in 0u8..13,
383        ) {
384            let mut sim_direct = GasSimulator::new();
385            sim_direct.feed_direct(cycles, decode_slots, src, 0xFF, dst);
386
387            let mut sim_feed = GasSimulator::new();
388            sim_feed.feed(&FastCost {
389                cycles,
390                decode_slots,
391                exec_unit: 1,
392                src_mask: 1u16 << src,
393                dst_mask: 1u16 << dst,
394                is_terminator: false,
395                is_move_reg: false,
396            });
397
398            prop_assert_eq!(
399                sim_direct.flush_and_get_cost(),
400                sim_feed.flush_and_get_cost()
401            );
402        }
403
404        /// Adding more instructions never decreases the cost.
405        #[test]
406        fn cost_monotonic_with_instructions(
407            base_count in 1usize..6,
408            extra_count in 1usize..4,
409            cycles in 1u8..10,
410        ) {
411            let mut sim_base = GasSimulator::new();
412            let mut sim_more = GasSimulator::new();
413            for i in 0..base_count.min(12) {
414                sim_base.feed_direct(cycles, 1, i as u8, 0xFF, (i + 1) as u8);
415                sim_more.feed_direct(cycles, 1, i as u8, 0xFF, (i + 1) as u8);
416            }
417            let base_cost = sim_base.flush_and_get_cost();
418            let last = base_count.min(12);
419            for i in 0..extra_count.min(12 - last) {
420                sim_more.feed_direct(cycles, 1, (last + i) as u8, 0xFF, (last + i + 1).min(12) as u8);
421            }
422            prop_assert!(sim_more.flush_and_get_cost() >= base_cost);
423        }
424    }
425}