#![cfg(feature = "jit")]
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use fusevm::{Chunk, ChunkBuilder, NumOp, Op, VMResult, Value, VM};
fn run(chunk: Chunk, hook: Option<fusevm::NumericHook>) -> Result<Value, String> {
let mut vm = VM::new(chunk);
vm.enable_tracing_jit();
if let Some(h) = hook {
vm.set_numeric_hook(h);
}
match vm.run() {
VMResult::Ok(v) => Ok(v),
VMResult::Halted => Ok(vm.stack.last().cloned().unwrap_or(Value::Undef)),
VMResult::Error(e) => Err(e),
}
}
fn float_chunk(f: f64) -> Chunk {
let mut b = ChunkBuilder::new();
b.emit(Op::LoadFloat(f), 0);
b.build()
}
fn push_value(bd: &mut ChunkBuilder, v: Value) {
match v {
Value::Int(n) => bd.emit(Op::LoadInt(n), 0),
Value::Float(f) => bd.emit(Op::LoadFloat(f), 0),
other => {
let idx = bd.add_constant(other);
bd.emit(Op::LoadConst(idx), 0)
}
};
}
fn unop_chunk(a: Value, op: Op) -> Chunk {
let mut bd = ChunkBuilder::new();
push_value(&mut bd, a);
bd.emit(op, 0);
bd.build()
}
fn binop_chunk(a: Value, b: Value, op: Op) -> Chunk {
let mut bd = ChunkBuilder::new();
match a {
Value::Int(n) => bd.emit(Op::LoadInt(n), 0),
Value::Float(f) => bd.emit(Op::LoadFloat(f), 0),
other => {
let idx = bd.add_constant(other);
bd.emit(Op::LoadConst(idx), 0)
}
};
match b {
Value::Int(n) => bd.emit(Op::LoadInt(n), 0),
Value::Float(f) => bd.emit(Op::LoadFloat(f), 0),
other => {
let idx = bd.add_constant(other);
bd.emit(Op::LoadConst(idx), 0)
}
};
bd.emit(op, 0);
bd.build()
}
#[test]
fn block_jit_preserves_a_float_chunk_result_across_runs() {
for i in 1..=12 {
let v = run(float_chunk(2.5), None).expect("float chunk runs");
assert_eq!(v, Value::Float(2.5), "run {i} lost the float");
}
assert_eq!(run(float_chunk(-0.5), None).unwrap(), Value::Float(-0.5));
}
#[test]
fn default_policy_still_coerces_and_wraps() {
let v = run(
binop_chunk(Value::Int(1), Value::str("a".to_string()), Op::Add),
None,
)
.expect("coercing add of a string does not error");
assert_eq!(v, Value::Float(1.0));
let v = run(
binop_chunk(Value::Int(i64::MAX), Value::Int(1), Op::Add),
None,
)
.expect("coercing add does not error on overflow");
assert_eq!(v, Value::Int(i64::MIN), "overflow must wrap, not trap");
}
#[test]
fn strict_mode_delegates_a_non_numeric_operand() {
let hook: fusevm::NumericHook = Arc::new(|_op, a, b| {
let bad = if matches!(a, Value::Int(_) | Value::Float(_)) {
b
} else {
a
};
Err(format!("wrong-type-argument: number-or-marker-p {bad:?}"))
});
let err = run(
binop_chunk(Value::Int(1), Value::str("a".to_string()), Op::Add),
Some(hook),
)
.expect_err("strict add of a string must signal");
assert!(
err.starts_with("wrong-type-argument"),
"hook's error must propagate verbatim, got: {err}"
);
}
#[test]
fn strict_mode_delegates_integer_overflow_even_once_jit_compiled() {
let calls = Arc::new(AtomicUsize::new(0));
let seen = calls.clone();
let hook: fusevm::NumericHook = Arc::new(move |op, a, b| {
seen.fetch_add(1, Ordering::Relaxed);
assert_eq!(op, NumOp::Add);
assert_eq!((a, b), (&Value::Int(i64::MAX), &Value::Int(1)));
Ok(Value::str("BIGNUM".to_string()))
});
for i in 1..=25 {
let v = run(
binop_chunk(Value::Int(i64::MAX), Value::Int(1), Op::Add),
Some(hook.clone()),
)
.expect("overflow is delegated, not an error");
assert_eq!(
v,
Value::str("BIGNUM".to_string()),
"run {i}: overflow escaped the hook (wrapped in native code?)"
);
}
assert_eq!(
calls.load(Ordering::Relaxed),
25,
"every run must delegate, including the JIT-compiled ones"
);
}
#[test]
fn strict_mode_checked_ops_are_exact_when_they_fit() {
let hook: fusevm::NumericHook = Arc::new(|_, _, _| Ok(Value::str("BIG".to_string())));
for i in 1..=15 {
let v = run(
binop_chunk(Value::Int(6), Value::Int(7), Op::Mul),
Some(hook.clone()),
)
.unwrap();
assert_eq!(v, Value::Int(42), "run {i}: checked mul changed the result");
}
let v = run(
binop_chunk(Value::Int(i64::MAX), Value::Int(2), Op::Mul),
Some(hook.clone()),
)
.unwrap();
assert_eq!(
v,
Value::str("BIG".to_string()),
"mul overflow must delegate"
);
}
#[test]
fn strict_mode_delegates_results_outside_a_narrowed_fixnum_range() {
const MOST_POSITIVE_FIXNUM: i64 = 2_305_843_009_213_693_951; const MOST_NEGATIVE_FIXNUM: i64 = -2_305_843_009_213_693_952;
let calls = Arc::new(AtomicUsize::new(0));
let seen = calls.clone();
let hook: fusevm::NumericHook = Arc::new(move |_op, _a, _b| {
seen.fetch_add(1, Ordering::Relaxed);
Ok(Value::str("BIGNUM".to_string()))
});
let run_ranged = |chunk: Chunk| -> Value {
let mut vm = VM::new(chunk);
vm.enable_tracing_jit();
vm.set_numeric_hook(hook.clone());
vm.set_fixnum_range(MOST_NEGATIVE_FIXNUM, MOST_POSITIVE_FIXNUM);
match vm.run() {
VMResult::Ok(v) => v,
VMResult::Halted => vm.stack.last().cloned().unwrap_or(Value::Undef),
VMResult::Error(e) => panic!("vm error: {e}"),
}
};
for i in 1..=25 {
let v = run_ranged(binop_chunk(
Value::Int(MOST_POSITIVE_FIXNUM),
Value::Int(1),
Op::Add,
));
assert_eq!(
v,
Value::str("BIGNUM".to_string()),
"run {i}: 2^61 escaped as a fixnum"
);
}
assert_eq!(calls.load(Ordering::Relaxed), 25);
let v = run_ranged(binop_chunk(
Value::Int(MOST_POSITIVE_FIXNUM - 1),
Value::Int(1),
Op::Add,
));
assert_eq!(v, Value::Int(MOST_POSITIVE_FIXNUM));
assert_eq!(
calls.load(Ordering::Relaxed),
25,
"in-range must not delegate"
);
}
fn counting_bignum_hook() -> (Arc<AtomicUsize>, fusevm::NumericHook) {
let calls = Arc::new(AtomicUsize::new(0));
let seen = calls.clone();
let hook: fusevm::NumericHook = Arc::new(move |_op, _a, _b| {
seen.fetch_add(1, Ordering::Relaxed);
Ok(Value::str("BIGNUM".to_string()))
});
(calls, hook)
}
#[test]
fn strict_mode_delegates_subtraction_overflow_across_jit() {
let (calls, hook) = counting_bignum_hook();
for i in 1..=25 {
let v = run(
binop_chunk(Value::Int(i64::MIN), Value::Int(1), Op::Sub),
Some(hook.clone()),
)
.expect("sub overflow is delegated, not an error");
assert_eq!(v, Value::str("BIGNUM".to_string()), "run {i}: sub overflow wrapped");
}
assert_eq!(calls.load(Ordering::Relaxed), 25, "every sub overflow must delegate");
}
#[test]
fn strict_mode_delegates_negate_overflow_but_not_in_range() {
let (calls, hook) = counting_bignum_hook();
for i in 1..=25 {
let v = run(unop_chunk(Value::Int(i64::MIN), Op::Negate), Some(hook.clone()))
.expect("negate overflow is delegated");
assert_eq!(v, Value::str("BIGNUM".to_string()), "run {i}: -i64::MIN wrapped");
}
assert_eq!(calls.load(Ordering::Relaxed), 25, "every -i64::MIN must delegate");
for i in 1..=15 {
let v = run(unop_chunk(Value::Int(42), Op::Negate), Some(hook.clone())).unwrap();
assert_eq!(v, Value::Int(-42), "run {i}: in-range negate changed");
}
assert_eq!(calls.load(Ordering::Relaxed), 25, "in-range negate must not delegate");
}
#[test]
fn strict_mode_delegates_negate_of_a_non_number() {
let err = run(
unop_chunk(Value::str("a".to_string()), Op::Negate),
Some(Arc::new(|op, a, b| {
assert_eq!(op, NumOp::Neg);
assert_eq!((a, b), (&Value::str("a".to_string()), &Value::Undef));
Err("wrong-type-argument".to_string())
})),
)
.expect_err("negating a string must signal in strict mode");
assert_eq!(err, "wrong-type-argument");
}
#[test]
fn strict_mode_delegates_below_the_narrowed_fixnum_range() {
const MOST_POSITIVE_FIXNUM: i64 = 2_305_843_009_213_693_951; const MOST_NEGATIVE_FIXNUM: i64 = -2_305_843_009_213_693_952;
let (calls, hook) = counting_bignum_hook();
let run_ranged = |chunk: Chunk| -> Value {
let mut vm = VM::new(chunk);
vm.enable_tracing_jit();
vm.set_numeric_hook(hook.clone());
vm.set_fixnum_range(MOST_NEGATIVE_FIXNUM, MOST_POSITIVE_FIXNUM);
match vm.run() {
VMResult::Ok(v) => v,
VMResult::Halted => vm.stack.last().cloned().unwrap_or(Value::Undef),
VMResult::Error(e) => panic!("vm error: {e}"),
}
};
for i in 1..=25 {
let v = run_ranged(binop_chunk(
Value::Int(MOST_NEGATIVE_FIXNUM),
Value::Int(1),
Op::Sub,
));
assert_eq!(v, Value::str("BIGNUM".to_string()), "run {i}: -2^61-1 escaped as fixnum");
}
assert_eq!(calls.load(Ordering::Relaxed), 25);
let v = run_ranged(binop_chunk(
Value::Int(MOST_NEGATIVE_FIXNUM + 1),
Value::Int(1),
Op::Sub,
));
assert_eq!(v, Value::Int(MOST_NEGATIVE_FIXNUM));
assert_eq!(calls.load(Ordering::Relaxed), 25, "in-range must not delegate");
}
#[test]
fn strict_mode_comparison_delegates_only_non_numbers() {
let err = run(
binop_chunk(Value::Int(1), Value::str("a".to_string()), Op::NumLt),
Some(Arc::new(|op, _a, _b| {
assert_eq!(op, NumOp::Lt);
Err("wrong-type-argument".to_string())
})),
)
.expect_err("comparing against a string must signal in strict mode");
assert_eq!(err, "wrong-type-argument");
let (calls, hook) = counting_bignum_hook();
for i in 1..=25 {
let v = run(
binop_chunk(Value::Int(3), Value::Int(7), Op::NumLt),
Some(hook.clone()),
)
.unwrap();
assert_eq!(v, Value::Bool(true), "run {i}: 3 < 7 wrong");
}
assert_eq!(calls.load(Ordering::Relaxed), 0, "numeric comparison must not delegate");
}
#[test]
fn strict_mode_div_and_pow_delegate_only_non_numbers() {
for op in [Op::Div, Op::Pow] {
let name = format!("{op:?}");
let (calls, hook) = counting_bignum_hook();
let v = run(
binop_chunk(Value::Int(2), Value::str("a".to_string()), op),
Some(hook.clone()),
)
.expect("delegated op returns the host value");
assert_eq!(v, Value::str("BIGNUM".to_string()), "{name} of a string must delegate");
assert_eq!(calls.load(Ordering::Relaxed), 1);
}
let (calls, hook) = counting_bignum_hook();
let v = run(binop_chunk(Value::Int(7), Value::Int(2), Op::Div), Some(hook)).unwrap();
assert_eq!(v, Value::Float(3.5));
assert_eq!(calls.load(Ordering::Relaxed), 0, "numeric divide must not delegate");
}
#[test]
fn strict_mode_never_delegates_exact_float_arithmetic() {
let hook: fusevm::NumericHook =
Arc::new(|op, a, b| panic!("float arithmetic delegated: {op:?} {a:?} {b:?}"));
for i in 1..=25 {
let v = run(
binop_chunk(Value::Int(i64::MAX), Value::Float(2.0), Op::Add),
Some(hook.clone()),
)
.expect("mixed float add stays native");
assert_eq!(v, Value::Float(i64::MAX as f64 + 2.0), "run {i}");
}
}