#![cfg(feature = "jit")]
use fusevm::{ChunkBuilder, Op, TraceJitConfig, VMResult, Value, VM};
struct Lcg {
state: u64,
}
impl Lcg {
fn new(seed: u64) -> Self {
Self {
state: seed.wrapping_mul(0x2545_F491_4F6C_DD1D) | 1,
}
}
fn next(&mut self) -> u64 {
self.state = self
.state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
self.state
}
fn range(&mut self, n: u64) -> u64 {
if n == 0 {
0
} else {
self.next() % n
}
}
}
fn build_random_loop(rng: &mut Lcg, limit: i64, body_len: usize) -> fusevm::Chunk {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadInt(0), 1);
b.emit(Op::SetSlot(0), 1);
b.emit(Op::LoadInt((rng.range(64) as i64) + 1), 1);
b.emit(Op::SetSlot(1), 1);
let anchor = b.current_pos();
b.emit(Op::PreIncSlotVoid(0), 1);
let mut depth: usize = 0;
for _ in 0..body_len {
let op = match depth {
0 => match rng.range(4) {
0 => Op::LoadInt(rng.range(0xFFFF) as i64),
1 => Op::GetSlot(0),
2 => Op::GetSlot(1),
_ => Op::Nop,
},
1 => match rng.range(6) {
0 => Op::LoadInt(rng.range(0xFFFF) as i64),
1 => Op::GetSlot(0),
2 => Op::GetSlot(1),
3 => Op::Pop,
4 => Op::Dup,
_ => Op::Nop,
},
_ => match rng.range(18) {
0 => Op::Add,
1 => Op::Sub,
2 => Op::Mul,
3 => Op::BitAnd,
4 => Op::BitOr,
5 => Op::BitXor,
6 => Op::NumEq,
7 => Op::NumNe,
8 => Op::NumLt,
9 => Op::NumGt,
10 => Op::Pop,
11 => Op::Dup,
12 => Op::SetSlot(rng.range(2) as u16),
13 => Op::Swap,
14 => Op::LoadInt(rng.range(0xFFFF) as i64),
15 => Op::GetSlot(0),
16 => Op::GetSlot(1),
_ => Op::AddAssignSlotVoid(rng.range(2) as u16, rng.range(2) as u16),
},
};
match &op {
Op::LoadInt(_) | Op::GetSlot(_) | Op::Dup => depth += 1,
Op::Pop => depth = depth.saturating_sub(1),
Op::SetSlot(_) => depth = depth.saturating_sub(1),
Op::Add
| Op::Sub
| Op::Mul
| Op::BitAnd
| Op::BitOr
| Op::BitXor
| Op::NumEq
| Op::NumNe
| Op::NumLt
| Op::NumGt => {
depth = depth.saturating_sub(1);
}
_ => {}
}
b.emit(op, 1);
}
while depth > 0 {
b.emit(Op::Pop, 1);
depth -= 1;
}
b.emit(Op::GetSlot(0), 1);
b.emit(Op::LoadInt(limit), 1);
b.emit(Op::NumLt, 1);
let close = b.emit(Op::JumpIfTrue(0), 1);
b.patch_jump(close, anchor);
b.emit(Op::GetSlot(0), 1);
b.build()
}
fn ensure_slots(vm: &mut VM, n: usize) {
let frame = vm.frames.last_mut().unwrap();
while frame.slots.len() < n {
frame.slots.push(Value::Int(0));
}
}
fn run_interp(chunk: &fusevm::Chunk) -> VMResult {
let mut vm = VM::new(chunk.clone());
ensure_slots(&mut vm, 2);
vm.run()
}
fn run_trace_jit(chunk: &fusevm::Chunk) -> VMResult {
let mut vm = VM::new(chunk.clone());
vm.enable_tracing_jit();
ensure_slots(&mut vm, 2);
vm.run()
}
fn results_equivalent(a: &VMResult, b: &VMResult) -> bool {
use VMResult::*;
match (a, b) {
(Ok(x), Ok(y)) => values_equivalent(x, y),
(Halted, Halted) => true,
(Error(_), Error(_)) => true,
_ => false,
}
}
fn values_equivalent(a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::Int(x), Value::Int(y)) => x == y,
(Value::Float(x), Value::Float(y)) => {
(x.is_nan() && y.is_nan()) || x == y
}
(Value::Int(x), Value::Float(y)) | (Value::Float(y), Value::Int(x)) => (*x as f64) == *y,
(Value::Bool(x), Value::Bool(y)) => x == y,
(Value::Undef, Value::Undef) => true,
_ => format!("{:?}", a) == format!("{:?}", b),
}
}
#[test]
fn fuzz_random_loops_match_interpreter() {
let jit = fusevm::JitCompiler::new();
let original_cfg = jit.get_config();
jit.set_config(TraceJitConfig {
trace_threshold: 5,
..original_cfg
});
let mut rng = Lcg::new(0xCAFE_BABE);
let mut diffs: Vec<(u64, VMResult, VMResult)> = Vec::new();
for case in 0..100 {
let seed_for_case = rng.next();
let mut case_rng = Lcg::new(seed_for_case);
let body_len = (case_rng.range(15) as usize) + 1;
let limit = (case_rng.range(80) as i64) + 50; let chunk = build_random_loop(&mut case_rng, limit, body_len);
let interp_result = run_interp(&chunk);
let jit_result = run_trace_jit(&chunk);
if !results_equivalent(&interp_result, &jit_result) {
diffs.push((seed_for_case, interp_result, jit_result));
}
if diffs.len() >= 5 {
break;
}
let _ = case;
}
jit.set_config(original_cfg);
if !diffs.is_empty() {
for (seed, interp, jit_r) in &diffs {
eprintln!(
"DIFF seed={:#x}\n interp = {:?}\n trace = {:?}",
seed, interp, jit_r
);
}
panic!(
"tracing JIT diverged from interpreter on {} fuzz case(s)",
diffs.len()
);
}
}
#[test]
fn fuzz_repeat_runs_are_deterministic() {
let mut rng = Lcg::new(0xFEED_FACE);
let body_len = (rng.range(10) as usize) + 3;
let limit = 80;
let chunk = build_random_loop(&mut rng, limit, body_len);
let r1 = run_trace_jit(&chunk);
let r2 = run_trace_jit(&chunk);
let r3 = run_trace_jit(&chunk);
assert!(
results_equivalent(&r1, &r2),
"two consecutive trace runs of the same chunk must agree: {:?} vs {:?}",
r1,
r2
);
assert!(
results_equivalent(&r2, &r3),
"third run must also agree: {:?} vs {:?}",
r2,
r3
);
}