use std::cell::RefCell;
use std::sync::OnceLock;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use crate::clock_mock;
use crate::stdlib::macros::harn_builtin;
use crate::testbench::tape::{self as tape, ClockSource, TapeRecordKind};
use crate::value::{VmError, VmValue};
use crate::vm::Vm;
static MONOTONIC_START: OnceLock<Instant> = OnceLock::new();
thread_local! {
static MOCK_TIME_GUARDS: RefCell<Vec<ScriptMockTimeGuard>> =
const { RefCell::new(Vec::new()) };
}
struct ScriptMockTimeGuard {
_clock: clock_mock::ClockOverrideGuard,
_leak_scope: clock_mock::leak_audit::ClockLeakScopeGuard,
}
fn real_wall_ms() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0)
}
fn real_monotonic_ms() -> i64 {
let start = MONOTONIC_START.get_or_init(Instant::now);
start.elapsed().as_millis() as i64
}
pub fn now_wall_ms() -> i64 {
let value = now_wall_ms_unrecorded();
record_clock_read(ClockSource::Wall, value);
value
}
pub(crate) fn now_wall_ms_unrecorded() -> i64 {
clock_mock::active_clock()
.map(|clock| harn_clock::now_wall_ms(clock.as_ref()))
.unwrap_or_else(real_wall_ms)
}
pub(crate) fn now_wall_ms_from(clock: &dyn harn_clock::Clock) -> i64 {
let value = harn_clock::now_wall_ms(clock);
record_clock_read_from(clock, ClockSource::Wall, value);
value
}
pub fn now_wall_seconds() -> f64 {
now_wall_ms() as f64 / 1000.0
}
pub fn now_monotonic_ms() -> i64 {
let value = clock_mock::active_clock()
.map(|clock| clock.monotonic_ms())
.unwrap_or_else(real_monotonic_ms);
record_clock_read(ClockSource::Monotonic, value);
value
}
pub(crate) fn now_monotonic_ms_from(clock: &dyn harn_clock::Clock) -> i64 {
let value = clock.monotonic_ms();
record_clock_read_from(clock, ClockSource::Monotonic, value);
value
}
pub(crate) fn record_clock_read(source: ClockSource, value_ms: i64) {
tape::with_active_recorder(|_recorder| Some(TapeRecordKind::ClockRead { source, value_ms }));
}
fn record_clock_read_from(clock: &dyn harn_clock::Clock, source: ClockSource, value_ms: i64) {
tape::with_active_recorder_clock(clock, |_recorder| {
Some(TapeRecordKind::ClockRead { source, value_ms })
});
}
pub(crate) fn record_clock_sleep(duration_ms: u64) {
tape::with_active_recorder(|_recorder| Some(TapeRecordKind::ClockSleep { duration_ms }));
}
pub(crate) fn record_clock_sleep_from(clock: &dyn harn_clock::Clock, duration_ms: u64) {
tape::with_active_recorder_clock(clock, |_recorder| {
Some(TapeRecordKind::ClockSleep { duration_ms })
});
}
pub fn is_mocked() -> bool {
clock_mock::is_mocked()
}
pub fn advance(ms: i64) {
if ms <= 0 {
return;
}
clock_mock::advance(Duration::from_millis(ms as u64));
record_clock_sleep(ms as u64);
}
fn push_mock(wall_ms: i64) {
let leak_scope = clock_mock::leak_audit::install_scope();
let clock = clock_mock::MockClock::at_wall_ms(wall_ms);
let guard = ScriptMockTimeGuard {
_clock: clock_mock::install_override(clock),
_leak_scope: leak_scope,
};
MOCK_TIME_GUARDS.with(|stack| stack.borrow_mut().push(guard));
}
fn pop_mock() -> bool {
MOCK_TIME_GUARDS.with(|stack| stack.borrow_mut().pop().is_some())
}
pub(crate) fn reset_clock_state() {
MOCK_TIME_GUARDS.with(|stack| stack.borrow_mut().clear());
clock_mock::clear_overrides();
}
#[allow(dead_code)]
pub struct MockClockGuard {
_override: clock_mock::ClockOverrideGuard,
}
#[allow(dead_code)]
impl MockClockGuard {
pub fn install(wall_ms: i64) -> Self {
let clock = clock_mock::MockClock::at_wall_ms(wall_ms);
let guard = clock_mock::install_override(clock);
Self { _override: guard }
}
pub fn advance(&self, ms: i64) {
advance(ms);
}
pub fn now_wall_ms(&self) -> i64 {
now_wall_ms()
}
pub fn now_monotonic_ms(&self) -> i64 {
now_monotonic_ms()
}
}
pub(crate) fn register_clock_builtins(vm: &mut Vm) {
crate::stdlib::macros::register_module_builtins(vm, module_path!());
}
#[harn_builtin(
exposure = "runtime_internal",
effects = [],
sig = "timestamp(...args: any) -> float", category = "clock"
)]
fn timestamp_impl(_args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
Ok(VmValue::Float(now_wall_seconds()))
}
#[harn_builtin(
exposure = "runtime_internal",
effects = [],
sig = "elapsed() -> int", category = "clock"
)]
fn elapsed_impl(_args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
Ok(VmValue::Int(now_monotonic_ms()))
}
#[harn_builtin(
exposure = "runtime_internal",
effects = [],
sig = "monotonic_ms(...args: any) -> int", category = "clock"
)]
fn monotonic_ms_impl(_args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
Ok(VmValue::Int(now_monotonic_ms()))
}
#[harn_builtin(
exposure = "runtime_internal",
effects = [],
sig = "now_ms(...args: any) -> int", category = "clock"
)]
fn now_ms_impl(_args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
Ok(VmValue::Int(now_wall_ms()))
}
#[harn_builtin(
exposure = "runtime_internal",
effects = [],
sig = "sleep_ms(...args: any) -> nil",
kind = "async",
category = "clock"
)]
async fn sleep_ms_impl(
_ctx: crate::vm::AsyncBuiltinCtx,
args: Vec<VmValue>,
) -> Result<VmValue, VmError> {
let ms = args.first().and_then(|a| a.as_int()).unwrap_or(0);
if ms <= 0 {
return Ok(VmValue::Nil);
}
if is_mocked() {
advance(ms);
} else {
tokio::time::sleep(Duration::from_millis(ms as u64)).await;
}
Ok(VmValue::Nil)
}
#[harn_builtin(
exposure = "runtime_internal",
effects = [],
sig = "mock_time(...args: any) -> nil", category = "clock"
)]
fn mock_time_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
let Some(ms) = args.first().and_then(|a| a.as_int()) else {
return Err(VmError::Thrown(VmValue::string(
"mock_time(ms): expected an int millisecond timestamp",
)));
};
push_mock(ms);
Ok(VmValue::Nil)
}
#[harn_builtin(
exposure = "runtime_internal",
effects = [],
sig = "advance_time(ms: int) -> int", category = "clock"
)]
fn advance_time_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
let ms = args.first().and_then(|a| a.as_int()).unwrap_or(0);
if !is_mocked() {
return Err(VmError::Thrown(VmValue::string(
"advance_time: no mock active. Call mock_time(ms) first.",
)));
}
advance(ms);
Ok(VmValue::Int(now_wall_ms()))
}
#[harn_builtin(
exposure = "runtime_internal",
effects = [],
sig = "unmock_time(...args: any) -> nil", category = "clock"
)]
fn unmock_time_impl(_args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
pop_mock();
Ok(VmValue::Nil)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mock_overrides_wall_and_monotonic() {
reset_clock_state();
push_mock(1_000_000);
assert_eq!(now_wall_ms(), 1_000_000);
assert_eq!(now_monotonic_ms(), 0);
advance(500);
assert_eq!(now_wall_ms(), 1_000_500);
assert_eq!(now_monotonic_ms(), 500);
reset_clock_state();
assert!(!is_mocked());
}
#[test]
fn unmocking_restores_host_time() {
reset_clock_state();
push_mock(123);
assert_eq!(now_wall_ms(), 123);
pop_mock();
assert!(!is_mocked());
assert!(now_wall_ms() > 123, "unmocking must restore the host clock");
}
#[test]
fn mock_clock_guard_restores_previous_state_on_drop() {
reset_clock_state();
assert!(!is_mocked());
{
let guard = MockClockGuard::install(2_000_000);
assert!(is_mocked());
assert_eq!(guard.now_wall_ms(), 2_000_000);
guard.advance(100);
assert_eq!(now_wall_ms(), 2_000_100);
assert_eq!(now_monotonic_ms(), 100);
}
assert!(!is_mocked(), "guard should clear mock on drop");
}
#[test]
fn mock_clock_guard_nests_and_restores_outer() {
reset_clock_state();
let outer = MockClockGuard::install(1_000);
outer.advance(50);
assert_eq!(now_wall_ms(), 1_050);
{
let inner = MockClockGuard::install(9_000);
inner.advance(5);
assert_eq!(now_wall_ms(), 9_005);
}
assert_eq!(now_wall_ms(), 1_050, "outer mock restored after inner drop");
drop(outer);
assert!(!is_mocked());
}
#[test]
fn stdlib_mock_visible_to_trigger_module() {
reset_clock_state();
push_mock(123_000);
let active = clock_mock::active_mock_clock().expect("active mock present");
assert_eq!(active.now_wall_ms(), 123_000);
advance(77);
assert_eq!(active.now_wall_ms(), 123_077);
reset_clock_state();
}
}