use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use fusevm::{Chunk, ChunkBuilder, Op, UndefRead, VMResult, Value, VM};
type Seen = Arc<std::sync::Mutex<Vec<(Option<String>, bool, usize)>>>;
fn recording_hook(seen: &Seen, answer: Result<Value, String>) -> fusevm::UndefHook {
let seen = Arc::clone(seen);
Arc::new(move |read: UndefRead<'_>| {
seen.lock().expect("seen lock").push((
read.name.map(str::to_string),
read.from_slot,
read.ip,
));
answer.clone()
})
}
fn run(chunk: Chunk, hook: Option<fusevm::UndefHook>) -> Result<Value, String> {
let mut vm = VM::new(chunk);
#[cfg(feature = "jit")]
vm.enable_tracing_jit();
if let Some(h) = hook {
vm.set_undef_hook(h);
}
match vm.run() {
VMResult::Ok(v) => Ok(v),
VMResult::Error(e) => Err(e),
VMResult::Halted => Err("halted".to_string()),
}
}
fn read_unset_global() -> Chunk {
let mut b = ChunkBuilder::new();
let x = b.add_name("x");
b.emit(Op::GetVar(x), 1);
b.build()
}
#[test]
fn without_a_hook_an_unset_global_reads_as_undef() {
assert_eq!(run(read_unset_global(), None), Ok(Value::Undef));
}
#[test]
fn the_hook_receives_the_globals_interned_name() {
let seen: Seen = Arc::default();
let err = run(
read_unset_global(),
Some(recording_hook(
&seen,
Err("can't read \"x\": no such variable".to_string()),
)),
);
assert_eq!(err, Err("can't read \"x\": no such variable".to_string()));
assert_eq!(
*seen.lock().expect("seen lock"),
vec![(Some("x".to_string()), false, 0)],
"a global read carries its name and is not flagged as a slot"
);
}
#[test]
fn the_hook_may_substitute_a_value_instead_of_refusing() {
let seen: Seen = Arc::default();
assert_eq!(
run(
read_unset_global(),
Some(recording_hook(&seen, Ok(Value::Int(7))))
),
Ok(Value::Int(7))
);
}
#[test]
fn answering_undef_is_the_default_reading() {
let seen: Seen = Arc::default();
assert_eq!(
run(
read_unset_global(),
Some(recording_hook(&seen, Ok(Value::Undef)))
),
Ok(Value::Undef),
"a host that declines to refuse gets exactly the hookless behaviour"
);
}
#[test]
fn a_slot_read_is_flagged_and_carries_no_name() {
let mut b = ChunkBuilder::new();
b.emit(Op::GetSlot(3), 1);
let seen: Seen = Arc::default();
let _ = run(b.build(), Some(recording_hook(&seen, Ok(Value::Undef))));
assert_eq!(
*seen.lock().expect("seen lock"),
vec![(None, true, 0)],
"a slot has no interned name; the host is told so rather than guessing"
);
}
#[test]
fn an_assigned_variable_never_reaches_the_hook() {
let mut b = ChunkBuilder::new();
let x = b.add_name("x");
b.emit(Op::LoadConst(0), 1);
b.emit(Op::SetVar(x), 1);
b.emit(Op::GetVar(x), 1);
let mut chunk = b.build();
chunk.constants.push(Value::Str(Arc::new(String::new())));
let seen: Seen = Arc::default();
let got = run(
chunk,
Some(recording_hook(&seen, Err("must not fire".to_string()))),
);
assert_eq!(got, Ok(Value::Str(Arc::new(String::new()))));
assert!(
seen.lock().expect("seen lock").is_empty(),
"an empty string is not an unset variable"
);
}
#[test]
#[cfg(feature = "jit")]
fn a_hot_loop_cannot_read_an_unset_global_natively() {
let calls = Arc::new(AtomicUsize::new(0));
let hook: fusevm::UndefHook = {
let calls = Arc::clone(&calls);
Arc::new(move |_read: UndefRead<'_>| {
calls.fetch_add(1, Ordering::Relaxed);
Err("unset".to_string())
})
};
let mut b = ChunkBuilder::new();
let i = b.add_name("i");
let never = b.add_name("never");
b.emit(Op::LoadInt(0), 1);
b.emit(Op::SetVar(i), 1);
let enter = b.emit(Op::Jump(usize::MAX), 1);
let body = b.current_pos();
b.emit(Op::GetVar(i), 1);
b.emit(Op::GetVar(never), 1);
b.emit(Op::Add, 1);
b.emit(Op::SetVar(i), 1);
let cond = b.current_pos();
b.patch_jump(enter, cond);
b.emit(Op::GetVar(i), 1);
b.emit(Op::LoadInt(100_000), 1);
b.emit(Op::NumLt, 1);
b.emit(Op::JumpIfTrue(body), 1);
b.emit(Op::GetVar(i), 1);
assert_eq!(run(b.build(), Some(hook)), Err("unset".to_string()));
assert_eq!(
calls.load(Ordering::Relaxed),
1,
"the first read refuses and ends the run; none is answered natively"
);
}
#[test]
fn a_frontend_can_refuse_one_read_site_and_tolerate_another() {
let mut b = ChunkBuilder::new();
let x = b.add_name("x");
let tolerant = b.emit(Op::GetVar(x), 1); b.emit(Op::Pop, 1);
b.emit(Op::GetVar(x), 1); let chunk = b.build();
let hook: fusevm::UndefHook = Arc::new(move |read: UndefRead<'_>| {
if read.ip == tolerant {
Ok(Value::Int(0))
} else {
Err(format!(
"can't read \"{}\": no such variable",
read.name.unwrap_or("?")
))
}
});
assert_eq!(
run(chunk, Some(hook)),
Err("can't read \"x\": no such variable".to_string()),
"the tolerant site passed and the refusing one raised"
);
}
#[test]
fn the_hook_can_tell_two_chunks_apart_at_the_same_ip() {
let seen: Arc<Mutex<Vec<(u64, usize)>>> = Arc::new(Mutex::new(Vec::new()));
let mut hashes = Vec::new();
for name in ["a", "b"] {
let mut b = ChunkBuilder::new();
let idx = b.add_name(name);
b.emit(Op::GetVar(idx), 1);
let chunk = b.build();
hashes.push(chunk.op_hash);
let log = Arc::clone(&seen);
let mut vm = VM::new(chunk);
vm.set_undef_hook(Arc::new(move |read: UndefRead<'_>| {
log.lock().expect("log").push((read.chunk, read.ip));
Ok(Value::Int(0))
}));
vm.run();
}
let seen = seen.lock().expect("log").clone();
assert_eq!(seen.len(), 2, "one read per chunk: {seen:?}");
assert_eq!(seen[0].1, seen[1].1, "both reads are at the same op index");
assert_ne!(
seen[0].0, seen[1].0,
"and the chunk hash is what separates them: {seen:?}"
);
assert_eq!(
hashes[0], hashes[1],
"the two chunks share an op vector, so `op_hash` alone would have \
collided — which is why the identity hashes the name pool too"
);
}