use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
#[crate::polydat_node(
category = Context,
purity = Nondeterministic("reads system clock"),
)]
fn current_epoch_millis() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as u64
}
#[crate::polydat_node(
category = Context,
purity = Nondeterministic("OS thread identity varies across fibers"),
)]
fn thread_id() -> u64 {
thread_local! {
static THREAD_ID: u64 = {
let id = std::thread::current().id();
let id_str = format!("{id:?}");
let num = id_str.trim_start_matches("ThreadId(").trim_end_matches(')');
num.parse().unwrap_or(0)
};
}
THREAD_ID.with(|id| *id)
}
#[crate::polydat_node(category = Context)]
fn env(name: Const<&str>) -> Result<String, String> {
let var = name.0;
std::env::var(var).map_err(|_| {
format!(
"env('{var}'): environment variable not set; \
use env_or('{var}', '<default>') if a fallback is acceptable",
)
})
}
#[crate::polydat_node(category = Context)]
fn env_or(
name: Const<&str>,
default: Const<&str>,
#[poly_const(capture_env_opt, from = name)] captured: &Option<String>,
) -> String {
match captured {
Some(v) => v.clone(),
None => default.0.to_string(),
}
}
fn capture_env_opt(name: &str) -> Option<String> {
std::env::var(name).ok()
}
#[crate::polydat_node(category = Context)]
fn tmp_dir(#[poly_const(capture_tmp_dir, from = ())] path: &String) -> String {
path.clone()
}
fn capture_tmp_dir() -> String {
std::env::temp_dir()
.to_str()
.map(String::from)
.unwrap_or_else(|| "/tmp".to_string())
}
#[crate::polydat_node(
category = Context,
purity = Nondeterministic("monotonic counter incremented per call"),
)]
fn counter(
#[poly_default(0u64)] start: Const<u64>,
#[poly_const(AtomicU64::new, from = start)] count: &AtomicU64,
) -> u64 {
count.fetch_add(1, Ordering::Relaxed)
}
#[crate::polydat_node(category = Context)]
fn limit(input: u64, max_items: Const<u64>) -> u64 {
let _ = max_items;
input
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{PolydatNode, Value};
#[test]
fn current_epoch_millis_reasonable() {
let node = CurrentEpochMillis::new();
let mut out = [Value::None];
node.eval(&[], &mut out);
let millis = out[0].as_u64();
assert!(millis > 1_704_067_200_000);
}
#[test]
fn counter_increments() {
let node = Counter::new(0);
let mut out = [Value::None];
node.eval(&[], &mut out);
assert_eq!(out[0].as_u64(), 0);
node.eval(&[], &mut out);
assert_eq!(out[0].as_u64(), 1);
node.eval(&[], &mut out);
assert_eq!(out[0].as_u64(), 2);
}
#[test]
fn counter_starting_at() {
let node = Counter::new(100);
let mut out = [Value::None];
node.eval(&[], &mut out);
assert_eq!(out[0].as_u64(), 100);
node.eval(&[], &mut out);
assert_eq!(out[0].as_u64(), 101);
}
fn unique_var(tag: &str) -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
format!("__NBRS_TEST_{tag}_{nanos:x}")
}
#[test]
fn env_captures_value_at_construction() {
let var = unique_var("ENV");
unsafe {
std::env::set_var(&var, "captured-value");
}
let node = Env::try_new(var.clone()).expect("env should read the set var");
unsafe {
std::env::set_var(&var, "later-value");
}
let mut out = [Value::None];
node.eval(&[], &mut out);
assert_eq!(out[0].as_str().to_string(), "captured-value");
unsafe {
std::env::remove_var(&var);
}
}
#[test]
fn env_errors_when_var_unset() {
let var = unique_var("ENV_MISSING");
unsafe {
std::env::remove_var(&var);
}
match Env::try_new(var.clone()) {
Ok(_) => panic!("Env::try_new should fail when the var is unset"),
Err(err) => {
assert!(
err.contains(&var),
"error should name the missing var: {err}"
);
assert!(
err.contains("env_or"),
"error should suggest env_or as the defaulted alternative: {err}"
);
}
}
}
#[test]
fn env_or_uses_default_when_var_unset() {
let var = unique_var("ENV_OR_MISSING");
unsafe {
std::env::remove_var(&var);
}
let node = EnvOr::new(var.clone(), "fallback".to_string());
let mut out = [Value::None];
node.eval(&[], &mut out);
assert_eq!(out[0].as_str().to_string(), "fallback");
}
#[test]
fn env_or_uses_var_value_when_set() {
let var = unique_var("ENV_OR_SET");
unsafe {
std::env::set_var(&var, "real-value");
}
let node = EnvOr::new(var.clone(), "fallback".to_string());
let mut out = [Value::None];
node.eval(&[], &mut out);
assert_eq!(out[0].as_str().to_string(), "real-value");
unsafe {
std::env::remove_var(&var);
}
}
#[test]
fn env_or_captures_at_construction_not_each_eval() {
let var = unique_var("ENV_OR_FROZEN");
unsafe {
std::env::set_var(&var, "first");
}
let node = EnvOr::new(var.clone(), "ignored-default".to_string());
unsafe {
std::env::set_var(&var, "second");
}
let mut out = [Value::None];
node.eval(&[], &mut out);
assert_eq!(
out[0].as_str().to_string(),
"first",
"env_or must freeze its value at construction; later env mutations are invisible"
);
unsafe {
std::env::remove_var(&var);
}
}
#[test]
fn tmp_dir_returns_a_path() {
let node = TmpDir::new();
let mut out = [Value::None];
node.eval(&[], &mut out);
let s = out[0].as_str().to_string();
assert!(!s.is_empty(), "tmp_dir() should produce a non-empty path");
}
#[test]
fn tmp_dir_is_stable_across_evals() {
let node = TmpDir::new();
let mut a = [Value::None];
let mut b = [Value::None];
node.eval(&[], &mut a);
node.eval(&[], &mut b);
assert_eq!(a[0].as_str(), b[0].as_str());
}
#[test]
fn env_or_compiles_through_dsl() {
let var = unique_var("DSL_ENV_OR");
unsafe {
std::env::set_var(&var, "x-value");
}
let src = format!("v := env_or(\"{var}\", \"fallback\")\n",);
let kernel = crate::dsl::compile_polydat_interpreter(&src).expect("compile env_or");
unsafe {
std::env::remove_var(&var);
}
let names = kernel.program().output_names();
assert!(names.contains(&"v"), "expected output 'v' in {names:?}");
}
#[test]
fn tmp_dir_compiles_through_dsl_in_string_template() {
let src = "path := \"{tmp_dir()}/data\"\n";
let kernel = crate::dsl::compile_polydat_interpreter(src)
.expect("compile tmp_dir() interpolated in a string");
let names = kernel.program().output_names();
assert!(
names.contains(&"path"),
"expected output 'path' in {names:?}"
);
}
}