polydat_core/library/
context.rs1use std::sync::atomic::{AtomicU64, Ordering};
26use std::time::{SystemTime, UNIX_EPOCH};
27
28#[crate::polydat_node(
32 category = Context,
33 purity = Nondeterministic("reads system clock"),
34)]
35fn current_epoch_millis() -> u64 {
36 SystemTime::now()
37 .duration_since(UNIX_EPOCH)
38 .unwrap()
39 .as_millis() as u64
40}
41
42fn capture_epoch_millis() -> u64 {
45 SystemTime::now()
46 .duration_since(UNIX_EPOCH)
47 .unwrap()
48 .as_millis() as u64
49}
50
51fn session_start_millis_jit_constants(node: &SessionStartMillis) -> Vec<u64> {
52 vec![node.start]
53}
54
55#[crate::polydat_node(
63 category = Context,
64 purity = Nondeterministic("session start time captured from system clock"),
65 jit_constants = session_start_millis_jit_constants,
66)]
67fn session_start_millis(#[poly_const(capture_epoch_millis, from = ())] start: &u64) -> u64 {
68 *start
69}
70
71fn elapsed_millis_jit_constants(node: &ElapsedMillis) -> Vec<u64> {
72 vec![node.start]
73}
74
75#[crate::polydat_node(
79 category = Context,
80 purity = Nondeterministic("monotonic elapsed time from system clock"),
81 jit_constants = elapsed_millis_jit_constants,
82)]
83fn elapsed_millis(#[poly_const(capture_epoch_millis, from = ())] start: &u64) -> u64 {
84 let now = SystemTime::now()
85 .duration_since(UNIX_EPOCH)
86 .unwrap()
87 .as_millis() as u64;
88 now.saturating_sub(*start)
89}
90
91#[crate::polydat_node(
96 category = Context,
97 purity = Nondeterministic("OS thread identity varies across fibers"),
98)]
99fn thread_id() -> u64 {
100 thread_local! {
101 static THREAD_ID: u64 = {
104 let id = std::thread::current().id();
105 let id_str = format!("{id:?}");
106 let num = id_str.trim_start_matches("ThreadId(").trim_end_matches(')');
107 num.parse().unwrap_or(0)
108 };
109 }
110 THREAD_ID.with(|id| *id)
111}
112
113#[crate::polydat_node(category = Context)]
125fn env(name: Const<&str>) -> Result<String, String> {
126 let var = name.0;
127 std::env::var(var).map_err(|_| {
128 format!(
129 "env('{var}'): environment variable not set; \
130 use env_or('{var}', '<default>') if a fallback is acceptable",
131 )
132 })
133}
134
135#[crate::polydat_node(category = Context)]
142fn env_or(
143 name: Const<&str>,
144 default: Const<&str>,
145 #[poly_const(capture_env_opt, from = name)] captured: &Option<String>,
146) -> String {
147 match captured {
148 Some(v) => v.clone(),
149 None => default.0.to_string(),
150 }
151}
152
153fn capture_env_opt(name: &str) -> Option<String> {
156 std::env::var(name).ok()
157}
158
159#[crate::polydat_node(category = Context)]
163fn tmp_dir(#[poly_const(capture_tmp_dir, from = ())] path: &String) -> String {
164 path.clone()
165}
166
167fn capture_tmp_dir() -> String {
171 std::env::temp_dir()
172 .to_str()
173 .map(String::from)
174 .unwrap_or_else(|| "/tmp".to_string())
175}
176
177#[crate::polydat_node(
181 category = Context,
182 purity = Nondeterministic("monotonic counter incremented per call"),
183)]
184fn counter(
185 #[poly_default(0u64)] start: Const<u64>,
186 #[poly_const(AtomicU64::new, from = start)] count: &AtomicU64,
187) -> u64 {
188 count.fetch_add(1, Ordering::Relaxed)
189}
190
191#[crate::polydat_node(category = Context)]
201fn limit(input: u64, max_items: Const<u64>) -> u64 {
202 let _ = max_items;
203 input
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209 use crate::ast::{PolydatNode, Value};
210
211 #[test]
212 fn current_epoch_millis_reasonable() {
213 let node = CurrentEpochMillis::new();
214 let mut out = [Value::None];
215 node.eval(&[], &mut out);
216 let millis = out[0].as_u64();
217 assert!(millis > 1_704_067_200_000);
219 }
220
221 #[test]
222 fn session_start_frozen() {
223 let node = SessionStartMillis::new();
224 let mut out1 = [Value::None];
225 let mut out2 = [Value::None];
226 node.eval(&[], &mut out1);
227 node.eval(&[], &mut out2);
228 assert_eq!(out1[0].as_u64(), out2[0].as_u64());
229 }
230
231 #[test]
232 fn elapsed_grows() {
233 let node = ElapsedMillis::new();
234 let mut out = [Value::None];
235 node.eval(&[], &mut out);
236 let e1 = out[0].as_u64();
237 assert!(e1 < 1000, "elapsed should be small right after creation");
239 }
240
241 #[test]
242 fn counter_increments() {
243 let node = Counter::new(0);
244 let mut out = [Value::None];
245 node.eval(&[], &mut out);
246 assert_eq!(out[0].as_u64(), 0);
247 node.eval(&[], &mut out);
248 assert_eq!(out[0].as_u64(), 1);
249 node.eval(&[], &mut out);
250 assert_eq!(out[0].as_u64(), 2);
251 }
252
253 #[test]
254 fn counter_starting_at() {
255 let node = Counter::new(100);
256 let mut out = [Value::None];
257 node.eval(&[], &mut out);
258 assert_eq!(out[0].as_u64(), 100);
259 node.eval(&[], &mut out);
260 assert_eq!(out[0].as_u64(), 101);
261 }
262
263 fn unique_var(tag: &str) -> String {
268 use std::time::{SystemTime, UNIX_EPOCH};
269 let nanos = SystemTime::now()
270 .duration_since(UNIX_EPOCH)
271 .unwrap()
272 .as_nanos();
273 format!("__NBRS_TEST_{tag}_{nanos:x}")
274 }
275
276 #[test]
277 fn env_captures_value_at_construction() {
278 let var = unique_var("ENV");
279 unsafe {
280 std::env::set_var(&var, "captured-value");
281 }
282 let node = Env::try_new(var.clone()).expect("env should read the set var");
283 unsafe {
286 std::env::set_var(&var, "later-value");
287 }
288 let mut out = [Value::None];
289 node.eval(&[], &mut out);
290 assert_eq!(out[0].as_str().to_string(), "captured-value");
291 unsafe {
292 std::env::remove_var(&var);
293 }
294 }
295
296 #[test]
297 fn env_errors_when_var_unset() {
298 let var = unique_var("ENV_MISSING");
299 unsafe {
300 std::env::remove_var(&var);
301 }
302 match Env::try_new(var.clone()) {
303 Ok(_) => panic!("Env::try_new should fail when the var is unset"),
304 Err(err) => {
305 assert!(
306 err.contains(&var),
307 "error should name the missing var: {err}"
308 );
309 assert!(
310 err.contains("env_or"),
311 "error should suggest env_or as the defaulted alternative: {err}"
312 );
313 }
314 }
315 }
316
317 #[test]
318 fn env_or_uses_default_when_var_unset() {
319 let var = unique_var("ENV_OR_MISSING");
320 unsafe {
321 std::env::remove_var(&var);
322 }
323 let node = EnvOr::new(var.clone(), "fallback".to_string());
324 let mut out = [Value::None];
325 node.eval(&[], &mut out);
326 assert_eq!(out[0].as_str().to_string(), "fallback");
327 }
328
329 #[test]
330 fn env_or_uses_var_value_when_set() {
331 let var = unique_var("ENV_OR_SET");
332 unsafe {
333 std::env::set_var(&var, "real-value");
334 }
335 let node = EnvOr::new(var.clone(), "fallback".to_string());
336 let mut out = [Value::None];
337 node.eval(&[], &mut out);
338 assert_eq!(out[0].as_str().to_string(), "real-value");
339 unsafe {
340 std::env::remove_var(&var);
341 }
342 }
343
344 #[test]
345 fn env_or_captures_at_construction_not_each_eval() {
346 let var = unique_var("ENV_OR_FROZEN");
347 unsafe {
348 std::env::set_var(&var, "first");
349 }
350 let node = EnvOr::new(var.clone(), "ignored-default".to_string());
351 unsafe {
352 std::env::set_var(&var, "second");
353 }
354 let mut out = [Value::None];
355 node.eval(&[], &mut out);
356 assert_eq!(
357 out[0].as_str().to_string(),
358 "first",
359 "env_or must freeze its value at construction; later env mutations are invisible"
360 );
361 unsafe {
362 std::env::remove_var(&var);
363 }
364 }
365
366 #[test]
367 fn tmp_dir_returns_a_path() {
368 let node = TmpDir::new();
369 let mut out = [Value::None];
370 node.eval(&[], &mut out);
371 let s = out[0].as_str().to_string();
372 assert!(!s.is_empty(), "tmp_dir() should produce a non-empty path");
373 }
374
375 #[test]
376 fn tmp_dir_is_stable_across_evals() {
377 let node = TmpDir::new();
378 let mut a = [Value::None];
379 let mut b = [Value::None];
380 node.eval(&[], &mut a);
381 node.eval(&[], &mut b);
382 assert_eq!(a[0].as_str(), b[0].as_str());
383 }
384
385 #[test]
388 fn env_or_compiles_through_dsl() {
389 let var = unique_var("DSL_ENV_OR");
390 unsafe {
391 std::env::set_var(&var, "x-value");
392 }
393 let src = format!("v := env_or(\"{var}\", \"fallback\")\n",);
394 let kernel = crate::dsl::compile_polydat_interpreter(&src).expect("compile env_or");
395 unsafe {
396 std::env::remove_var(&var);
397 }
398 let names = kernel.program().output_names();
403 assert!(names.contains(&"v"), "expected output 'v' in {names:?}");
404 }
405
406 #[test]
407 fn tmp_dir_compiles_through_dsl_in_string_template() {
408 let src = "path := \"{tmp_dir()}/data\"\n";
413 let kernel = crate::dsl::compile_polydat_interpreter(src)
414 .expect("compile tmp_dir() interpolated in a string");
415 let names = kernel.program().output_names();
416 assert!(
417 names.contains(&"path"),
418 "expected output 'path' in {names:?}"
419 );
420 }
421}