use lex_ast::canonicalize_program;
use lex_bytecode::{compile_program, vm::Vm, Value};
use lex_runtime::{DefaultHandler, Policy};
use lex_syntax::parse_source;
use std::collections::BTreeSet;
use std::sync::Mutex;
fn env_lock() -> &'static Mutex<()> {
static LOCK: std::sync::OnceLock<Mutex<()>> = std::sync::OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
fn policy_with_time() -> Policy {
let mut p = Policy::pure();
p.allow_effects = ["time".to_string()].into_iter().collect::<BTreeSet<_>>();
p
}
fn run(src: &str, fn_name: &str) -> Value {
let prog = parse_source(src).expect("parse");
let stages = canonicalize_program(&prog);
if let Err(errs) = lex_types::check_program(&stages) {
panic!("type errors:\n{errs:#?}");
}
let bc = compile_program(&stages);
let handler = DefaultHandler::new(policy_with_time());
let mut vm = Vm::with_handler(&bc, Box::new(handler));
vm.call(fn_name, vec![]).unwrap_or_else(|e| panic!("call {fn_name}: {e}"))
}
const SRC: &str = r#"
import "std.time" as time
fn ms_now() -> [time] Int { time.now_ms() }
fn str_now() -> [time] Str { time.now_str() }
fn mono_now() -> [time] Int { time.mono_ns() }
# Two reads of the monotonic clock back-to-back. The second one must
# be >= the first.
fn mono_pair() -> [time] (Int, Int) {
let a := time.mono_ns()
let b := time.mono_ns()
(a, b)
}
"#;
#[test]
fn now_ms_returns_positive_unix_millis() {
let _g = env_lock().lock().unwrap_or_else(|p| p.into_inner());
let prior = std::env::var("LEX_TEST_NOW").ok();
std::env::remove_var("LEX_TEST_NOW");
let v = run(SRC, "ms_now");
if let Some(s) = prior { std::env::set_var("LEX_TEST_NOW", s); }
let ms = match v {
Value::Int(n) => n,
other => panic!("expected Int, got {other:?}"),
};
assert!(
ms > 1_577_836_800_000,
"time.now_ms should return a recent Unix-millis value; got {ms}"
);
}
#[test]
fn now_str_returns_iso8601_utc() {
let _g = env_lock().lock().unwrap_or_else(|p| p.into_inner());
let prior = std::env::var("LEX_TEST_NOW").ok();
std::env::remove_var("LEX_TEST_NOW");
let v = run(SRC, "str_now");
if let Some(s) = prior { std::env::set_var("LEX_TEST_NOW", s); }
let s = match v {
Value::Str(s) => s,
other => panic!("expected Str, got {other:?}"),
};
assert!(
s.len() >= 20,
"time.now_str should look like an ISO-8601 string; got {s:?}"
);
assert!(
s.contains('T'),
"time.now_str output should have a 'T' separating date and time; got {s:?}"
);
assert!(
s.ends_with('Z') || s.ends_with("+00:00"),
"time.now_str should be in UTC (ending with 'Z' or '+00:00'); got {s:?}"
);
let _: chrono::DateTime<chrono::Utc> = s.parse().unwrap_or_else(|e| {
panic!("time.now_str output {s:?} is not a valid RFC 3339 timestamp: {e}")
});
}
#[test]
fn mono_ns_is_non_decreasing_across_calls() {
let _g = env_lock().lock().unwrap_or_else(|p| p.into_inner());
let v = run(SRC, "mono_pair");
let (a, b) = match v {
Value::Tuple(xs) if xs.len() == 2 => {
let a = match &xs[0] { Value::Int(n) => *n, other => panic!("got {other:?}") };
let b = match &xs[1] { Value::Int(n) => *n, other => panic!("got {other:?}") };
(a, b)
}
other => panic!("expected Tuple, got {other:?}"),
};
assert!(a >= 0, "first mono_ns reading must be non-negative; got {a}");
assert!(b >= a, "mono_ns must be non-decreasing; got first={a}, second={b}");
}
#[test]
fn now_ms_respects_lex_test_now() {
let _g = env_lock().lock().unwrap_or_else(|p| p.into_inner());
let prior = std::env::var("LEX_TEST_NOW").ok();
std::env::set_var("LEX_TEST_NOW", "1700000000");
let v = run(SRC, "ms_now");
match prior {
Some(s) => std::env::set_var("LEX_TEST_NOW", s),
None => std::env::remove_var("LEX_TEST_NOW"),
}
let ms = match v {
Value::Int(n) => n,
other => panic!("expected Int, got {other:?}"),
};
assert_eq!(
ms, 1_700_000_000_000,
"time.now_ms should lift LEX_TEST_NOW seconds to ms by ×1000"
);
}
#[test]
fn now_str_respects_lex_test_now() {
let _g = env_lock().lock().unwrap_or_else(|p| p.into_inner());
let prior = std::env::var("LEX_TEST_NOW").ok();
std::env::set_var("LEX_TEST_NOW", "1577836800");
let v = run(SRC, "str_now");
match prior {
Some(s) => std::env::set_var("LEX_TEST_NOW", s),
None => std::env::remove_var("LEX_TEST_NOW"),
}
let s = match v {
Value::Str(s) => s,
other => panic!("expected Str, got {other:?}"),
};
assert!(
s.starts_with("2020-01-01T00:00:00"),
"expected pinned timestamp to render as 2020-01-01T00:00:00...; got {s:?}"
);
}