use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::ast::{PolydatNode, NodeMeta, Port, Slot, SlotType, Value};
use crate::derive_support::Const;
#[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
}
fn capture_epoch_millis() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as u64
}
fn session_start_millis_jit_constants(node: &SessionStartMillis) -> Vec<u64> {
vec![node.start]
}
#[crate::polydat_node(
category = Context,
purity = Nondeterministic("session start time captured from system clock"),
jit_constants = session_start_millis_jit_constants,
)]
fn session_start_millis(
#[poly_const(capture_epoch_millis, from = ())]
start: &u64,
) -> u64 {
*start
}
fn elapsed_millis_jit_constants(node: &ElapsedMillis) -> Vec<u64> {
vec![node.start]
}
#[crate::polydat_node(
category = Context,
purity = Nondeterministic("monotonic elapsed time from system clock"),
jit_constants = elapsed_millis_jit_constants,
)]
fn elapsed_millis(
#[poly_const(capture_epoch_millis, from = ())]
start: &u64,
) -> u64 {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
now.saturating_sub(*start)
}
#[crate::polydat_node(
category = Context,
purity = Nondeterministic("OS thread identity varies across fibers"),
)]
fn 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)
}
#[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)
}
pub struct CursorLimit {
meta: NodeMeta,
pub max_items: u64,
}
impl CursorLimit {
pub fn new(max_items: u64) -> Self {
Self {
meta: NodeMeta {
name: "limit".into(),
outs: vec![Port::u64("output")],
ins: vec![Slot::Wire(Port::u64("input"))],
},
max_items,
}
}
}
impl PolydatNode for CursorLimit {
fn meta(&self) -> &NodeMeta { &self.meta }
fn eval(&self, inputs: &[Value], outputs: &mut [Value]) {
outputs[0] = inputs[0].clone();
}
}
use crate::dsl::registry::{Arity, FuncCategory, FuncSig, ParamSpec};
pub fn signatures() -> &'static [FuncSig] {
use FuncCategory as C;
&[
FuncSig {
name: "limit", category: C::Context, outputs: 1,
description: "cursor limit — clamps extent for smoke testing",
help: "Passes through the input value unchanged. Inserted by the compiler\n\
when the `limit` activity parameter is present. The max_items value\n\
is used by the cursor system to stop advancing early.\n\
Parameters:\n input — cursor wire (u64)\n max_items — maximum items to yield\n\
Example: row = limit(row, 100) // stop after 100 items",
identity: None, variadic_ctor: None,
params: &[
ParamSpec { name: "input", slot_type: SlotType::Wire, required: true, example: "row", constraint: None },
ParamSpec { name: "max_items", slot_type: SlotType::ConstU64, required: true, example: "100", constraint: None },
],
arity: Arity::Fixed,
commutativity: crate::ast::Commutativity::Positional,
default_resolver: None,
output_type: crate::dsl::registry::OutputType::Fixed,
output_port: None,
},
]
}
pub(crate) fn build_node(name: &str, _wires: &[crate::compile::assembly::WireRef], _wire_types: &[crate::ast::PortType], consts: &[crate::dsl::factory::ConstArg]) -> Option<Result<Box<dyn crate::ast::PolydatNode>, String>> {
match name {
"limit" => {
let max_items = consts.first().map(|c| c.as_u64()).unwrap_or(u64::MAX);
Some(Ok(Box::new(CursorLimit::new(max_items))))
}
_ => None,
}
}
crate::register_nodes!(signatures, build_node);
#[cfg(test)]
mod tests {
use super::*;
#[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 session_start_frozen() {
let node = SessionStartMillis::new();
let mut out1 = [Value::None];
let mut out2 = [Value::None];
node.eval(&[], &mut out1);
node.eval(&[], &mut out2);
assert_eq!(out1[0].as_u64(), out2[0].as_u64());
}
#[test]
fn elapsed_grows() {
let node = ElapsedMillis::new();
let mut out = [Value::None];
node.eval(&[], &mut out);
let e1 = out[0].as_u64();
assert!(e1 < 1000, "elapsed should be small right after creation");
}
#[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(&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(src)
.expect("compile tmp_dir() interpolated in a string");
let names = kernel.program().output_names();
assert!(names.contains(&"path"), "expected output 'path' in {names:?}");
}
#[test]
fn elapsed_from_injected_origin_compiles_in_dsl() {
let src = "extern phase_start: u64 = 0\n\
volatile te := current_epoch_millis() - phase_start\n";
let k = crate::dsl::compile_polydat(src)
.expect("clock-minus-injected-origin must compile");
assert!(k.program().output_names().contains(&"te"),
"expected output 'te' in {:?}", k.program().output_names());
}
}