#![cfg(feature = "aot")]
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use fusevm::{Chunk, ChunkBuilder, Op, VMResult, Value, VM};
fn both_tiers(chunk: &Chunk, configure: impl Fn(&mut VM) + Copy + 'static) -> (Value, Value) {
let native = match fusevm::aot::run_chunk_native(chunk, configure).expect("native run") {
VMResult::Ok(v) => v,
other => panic!("native: unexpected {other:?}"),
};
let mut vm = VM::new(chunk.clone());
configure(&mut vm);
let interpreted = match vm.run() {
VMResult::Ok(v) => v,
VMResult::Halted => vm.stack.last().cloned().unwrap_or(Value::Undef),
VMResult::Error(e) => panic!("interp error: {e}"),
};
(native, interpreted)
}
fn wrapped(a: i64, b: i64, op: Op, bits: u32) -> Chunk {
let mut bd = ChunkBuilder::new();
bd.emit(Op::LoadInt(a), 0);
bd.emit(Op::LoadInt(b), 0);
bd.emit(op, 0);
let sh = (64 - bits) as i64;
bd.emit(Op::LoadInt(sh), 0);
bd.emit(Op::Shl, 0);
bd.emit(Op::LoadInt(sh), 0);
bd.emit(Op::Shr, 0);
bd.build()
}
#[test]
fn bytecode_sign_extend_agrees_between_aot_and_interpreter() {
let cases: &[(u32, i64, Op, i64, i64)] = &[
(32, i32::MAX as i64, Op::Add, 1, i32::MIN as i64),
(32, i32::MIN as i64, Op::Sub, 1, i32::MAX as i64),
(32, 1_000_000, Op::Mul, 1_000_000, -727_379_968),
(8, 127, Op::Add, 1, -128),
(16, 32767, Op::Add, 1, -32768),
(32, 2_000_000, Op::Add, 3_000_000, 5_000_000),
];
for (bits, a, op, b, want) in cases {
let (bits, a, b, want) = (*bits, *a, *b, *want);
let chunk = wrapped(a, b, op.clone(), bits);
let (native, interp) = both_tiers(&chunk, |_vm| {});
assert_eq!(
native, interp,
"{bits}-bit {a} {op:?} {b}: AOT and interpreter must agree"
);
assert_eq!(
native,
Value::Int(want),
"{bits}-bit {a} {op:?} {b}: must match the reference language"
);
}
}
#[test]
fn bitand_mask_agrees_between_aot_and_interpreter() {
let mut bd = ChunkBuilder::new();
bd.emit(Op::LoadInt(0), 0);
bd.emit(Op::LoadInt(1), 0);
bd.emit(Op::Sub, 0);
bd.emit(Op::LoadInt(0xFF), 0);
bd.emit(Op::BitAnd, 0);
let chunk = bd.build();
let (native, interp) = both_tiers(&chunk, |_vm| {});
assert_eq!(native, interp);
assert_eq!(native, Value::Int(255), "Go: var u8 uint8 = 0; u8-- == 255");
}
#[test]
fn known_divergence_aot_ignores_the_fixnum_range() {
let mut bd = ChunkBuilder::new();
bd.emit(Op::LoadInt(i32::MAX as i64), 0);
bd.emit(Op::LoadInt(1), 0);
bd.emit(Op::Add, 0);
bd.set_int_overflow_deopt(true);
let chunk = bd.build();
let calls = Arc::new(AtomicUsize::new(0));
let native_calls = calls.clone();
let native = match fusevm::aot::run_chunk_native(&chunk, move |vm: &mut VM| {
let c = native_calls.clone();
vm.set_numeric_hook(Arc::new(move |_op, a, b| {
c.fetch_add(1, Ordering::Relaxed);
let n = a.to_int().wrapping_add(b.to_int());
Ok(Value::Int((n << 32) >> 32))
}));
vm.set_fixnum_range(i32::MIN as i64, i32::MAX as i64);
})
.expect("native run")
{
VMResult::Ok(v) => v,
other => panic!("native: unexpected {other:?}"),
};
assert_eq!(
native,
Value::Int(2_147_483_648),
"recorded behavior: AOT keeps the i64 result"
);
assert_eq!(
calls.load(Ordering::Relaxed),
0,
"recorded behavior: AOT never consults the hook for an in-i64 result"
);
let interp_calls = Arc::new(AtomicUsize::new(0));
let c = interp_calls.clone();
let mut vm = VM::new(chunk);
vm.set_numeric_hook(Arc::new(move |_op, a, b| {
c.fetch_add(1, Ordering::Relaxed);
let n = a.to_int().wrapping_add(b.to_int());
Ok(Value::Int((n << 32) >> 32))
}));
vm.set_fixnum_range(i32::MIN as i64, i32::MAX as i64);
let interp = match vm.run() {
VMResult::Ok(v) => v,
other => panic!("interp: unexpected {other:?}"),
};
assert_eq!(interp, Value::Int(i32::MIN as i64));
assert_eq!(interp_calls.load(Ordering::Relaxed), 1);
assert_ne!(
native, interp,
"if these now agree, AOT learned the range — rewrite this test as assert_eq"
);
}