use luna_core::runtime::Value;
use luna_core::version::LuaVersion;
use luna_core::vm::exec::{
HOOK_MASK_CALL, HOOK_MASK_COUNT, HOOK_MASK_LINE, HOOK_MASK_RETURN, RustDebugHook, RustHookEvent,
};
use luna_core::vm::{LuaError, Vm};
use std::cell::RefCell;
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
fn noop_waker() -> Waker {
fn noop(_: *const ()) {}
fn clone(_: *const ()) -> RawWaker {
raw()
}
fn raw() -> RawWaker {
static VT: RawWakerVTable = RawWakerVTable::new(clone, noop, noop, noop);
RawWaker::new(std::ptr::null(), &VT)
}
unsafe { Waker::from_raw(raw()) }
}
fn block_on<F: Future>(mut fut: F) -> F::Output {
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
let mut fut = unsafe { Pin::new_unchecked(&mut fut) };
loop {
match fut.as_mut().poll(&mut cx) {
Poll::Ready(v) => return v,
Poll::Pending => continue,
}
}
}
struct YieldOnce {
yielded: bool,
}
impl YieldOnce {
fn new() -> Self {
YieldOnce { yielded: false }
}
}
impl Future for YieldOnce {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
if self.yielded {
Poll::Ready(())
} else {
self.yielded = true;
cx.waker().wake_by_ref();
Poll::Pending
}
}
}
thread_local! {
static EVENTS: RefCell<Vec<RustHookEvent>> = const { RefCell::new(Vec::new()) };
}
fn record_hook(_vm: &mut Vm, event: RustHookEvent) {
EVENTS.with(|e| e.borrow_mut().push(event));
}
fn snapshot_events() -> Vec<RustHookEvent> {
EVENTS.with(|e| e.borrow().clone())
}
fn clear_events() {
EVENTS.with(|e| e.borrow_mut().clear());
}
fn count_calls(evts: &[RustHookEvent]) -> usize {
evts.iter()
.filter(|e| matches!(e, RustHookEvent::Call))
.count()
}
fn count_returns(evts: &[RustHookEvent]) -> usize {
evts.iter()
.filter(|e| matches!(e, RustHookEvent::Return))
.count()
}
fn an_ready_42(
vm: *mut Vm,
func_slot: u32,
_nargs: u32,
) -> Pin<Box<dyn Future<Output = Result<u32, LuaError>>>> {
Box::pin(async move {
let vm = unsafe { &mut *vm };
vm.nat_return(func_slot, &[Value::Int(42)]);
Ok(1)
})
}
fn an_yield_then_7(
vm: *mut Vm,
func_slot: u32,
_nargs: u32,
) -> Pin<Box<dyn Future<Output = Result<u32, LuaError>>>> {
Box::pin(async move {
YieldOnce::new().await;
let vm = unsafe { &mut *vm };
vm.nat_return(func_slot, &[Value::Int(7)]);
Ok(1)
})
}
#[test]
fn call_and_return_fire_around_async_native_ready_path() {
clear_events();
let mut vm = Vm::sandbox(LuaVersion::Lua55).open_base().build();
vm.set_rust_debug_hook(Some(record_hook), HOOK_MASK_CALL | HOOK_MASK_RETURN, 0);
vm.set_async_native("ret42", an_ready_42).unwrap();
let r = block_on(vm.eval_async("return ret42()")).unwrap();
assert_eq!(r.len(), 1);
matches!(r[0], Value::Int(42));
let evts = snapshot_events();
let calls = count_calls(&evts);
let rets = count_returns(&evts);
assert!(
calls >= 2 && rets >= 2,
"expected ≥2 Call + ≥2 Return events bracketing chunk + async native, got {evts:?}"
);
}
#[test]
fn call_and_return_bracket_async_native_yield_path() {
clear_events();
let mut vm = Vm::sandbox(LuaVersion::Lua55).open_base().build();
vm.set_rust_debug_hook(Some(record_hook), HOOK_MASK_CALL | HOOK_MASK_RETURN, 0);
vm.set_async_native("yield7", an_yield_then_7).unwrap();
let r = block_on(vm.eval_async("return yield7() + 1")).unwrap();
assert_eq!(r.len(), 1);
let n = match r[0] {
Value::Int(n) => n,
Value::Float(f) => f as i64,
other => panic!("non-numeric: {other:?}"),
};
assert_eq!(n, 8);
let evts = snapshot_events();
let first_call = evts
.iter()
.position(|e| matches!(e, RustHookEvent::Call))
.expect("at least one Call should fire");
let last_return = evts
.iter()
.rposition(|e| matches!(e, RustHookEvent::Return))
.expect("at least one Return should fire");
assert!(
last_return > first_call,
"Return must follow Call (got {evts:?})"
);
}
#[test]
fn count_hook_carries_across_async_slice_boundaries() {
clear_events();
let mut vm = Vm::sandbox(LuaVersion::Lua55).open_base().build();
vm.set_async_slice(50);
vm.set_rust_debug_hook(Some(record_hook), HOOK_MASK_COUNT, 100);
let _ = block_on(vm.eval_async("local s = 0; for i = 1, 500 do s = s + i end")).unwrap();
let counts = snapshot_events()
.iter()
.filter(|e| matches!(e, RustHookEvent::Count))
.count();
assert!(
counts >= 1,
"count hook must fire across slice boundaries (got {counts} events)"
);
}
#[test]
fn line_hook_dedupes_across_async_slice_boundaries() {
clear_events();
let mut vm = Vm::sandbox(LuaVersion::Lua55).open_base().build();
vm.set_async_slice(3);
vm.set_rust_debug_hook(Some(record_hook), HOOK_MASK_LINE, 0);
let _ = block_on(vm.eval_async("local a = 1\nlocal b = 2\nlocal c = a + b\nreturn c")).unwrap();
let lines: Vec<u32> = snapshot_events()
.iter()
.filter_map(|e| match e {
RustHookEvent::Line(n) => Some(*n),
_ => None,
})
.collect();
assert!(
!lines.is_empty(),
"expected at least one Line event, got none"
);
let mut sorted = lines.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(
lines.len(),
sorted.len(),
"Line hook double-fired across slice boundary: {lines:?}"
);
}
#[test]
fn rust_debug_hook_is_send_at_type_level() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<RustDebugHook>();
assert_sync::<RustDebugHook>();
assert_send::<RustHookEvent>();
}
#[test]
fn hook_call_returning_err_aborts_async_native() {
fn err_on_call(vm: &mut Vm, evt: RustHookEvent) {
if matches!(evt, RustHookEvent::Call) {
let _ = vm.set_global("hook_saw_call", Value::Bool(true));
}
}
let mut vm = Vm::sandbox(LuaVersion::Lua55).open_base().build();
vm.set_rust_debug_hook(Some(err_on_call), HOOK_MASK_CALL, 0);
vm.set_async_native("ret42", an_ready_42).unwrap();
let r = block_on(vm.eval_async("return ret42()")).unwrap();
assert_eq!(r.len(), 1);
let key = Value::Str(vm.intern_str("hook_saw_call"));
let saw = vm.globals().get(key);
assert!(
matches!(saw, Value::Bool(true)),
"hook should have recorded the async-native Call event, saw {saw:?}"
);
}