1use std::sync::atomic::{AtomicU64, Ordering};
26use std::time::{SystemTime, UNIX_EPOCH};
27
28use crate::ast::{NodeMeta, PolydatNode, Port, Slot, SlotType, Value};
29
30#[crate::polydat_node(
34 category = Context,
35 purity = Nondeterministic("reads system clock"),
36)]
37fn current_epoch_millis() -> u64 {
38 SystemTime::now()
39 .duration_since(UNIX_EPOCH)
40 .unwrap()
41 .as_millis() as u64
42}
43
44fn capture_epoch_millis() -> u64 {
47 SystemTime::now()
48 .duration_since(UNIX_EPOCH)
49 .unwrap()
50 .as_millis() as u64
51}
52
53fn session_start_millis_jit_constants(node: &SessionStartMillis) -> Vec<u64> {
54 vec![node.start]
55}
56
57#[crate::polydat_node(
65 category = Context,
66 purity = Nondeterministic("session start time captured from system clock"),
67 jit_constants = session_start_millis_jit_constants,
68)]
69fn session_start_millis(#[poly_const(capture_epoch_millis, from = ())] start: &u64) -> u64 {
70 *start
71}
72
73fn elapsed_millis_jit_constants(node: &ElapsedMillis) -> Vec<u64> {
74 vec![node.start]
75}
76
77#[crate::polydat_node(
81 category = Context,
82 purity = Nondeterministic("monotonic elapsed time from system clock"),
83 jit_constants = elapsed_millis_jit_constants,
84)]
85fn elapsed_millis(#[poly_const(capture_epoch_millis, from = ())] start: &u64) -> u64 {
86 let now = SystemTime::now()
87 .duration_since(UNIX_EPOCH)
88 .unwrap()
89 .as_millis() as u64;
90 now.saturating_sub(*start)
91}
92
93#[crate::polydat_node(
98 category = Context,
99 purity = Nondeterministic("OS thread identity varies across fibers"),
100)]
101fn thread_id() -> u64 {
102 thread_local! {
103 static THREAD_ID: u64 = {
106 let id = std::thread::current().id();
107 let id_str = format!("{id:?}");
108 let num = id_str.trim_start_matches("ThreadId(").trim_end_matches(')');
109 num.parse().unwrap_or(0)
110 };
111 }
112 THREAD_ID.with(|id| *id)
113}
114
115#[crate::polydat_node(category = Context)]
127fn env(name: Const<&str>) -> Result<String, String> {
128 let var = name.0;
129 std::env::var(var).map_err(|_| {
130 format!(
131 "env('{var}'): environment variable not set; \
132 use env_or('{var}', '<default>') if a fallback is acceptable",
133 )
134 })
135}
136
137#[crate::polydat_node(category = Context)]
144fn env_or(
145 name: Const<&str>,
146 default: Const<&str>,
147 #[poly_const(capture_env_opt, from = name)] captured: &Option<String>,
148) -> String {
149 match captured {
150 Some(v) => v.clone(),
151 None => default.0.to_string(),
152 }
153}
154
155fn capture_env_opt(name: &str) -> Option<String> {
158 std::env::var(name).ok()
159}
160
161#[crate::polydat_node(category = Context)]
165fn tmp_dir(#[poly_const(capture_tmp_dir, from = ())] path: &String) -> String {
166 path.clone()
167}
168
169fn capture_tmp_dir() -> String {
173 std::env::temp_dir()
174 .to_str()
175 .map(String::from)
176 .unwrap_or_else(|| "/tmp".to_string())
177}
178
179#[crate::polydat_node(
183 category = Context,
184 purity = Nondeterministic("monotonic counter incremented per call"),
185)]
186fn counter(
187 #[poly_default(0u64)] start: Const<u64>,
188 #[poly_const(AtomicU64::new, from = start)] count: &AtomicU64,
189) -> u64 {
190 count.fetch_add(1, Ordering::Relaxed)
191}
192
193pub struct CursorLimit {
211 meta: NodeMeta,
212 pub max_items: u64,
214}
215
216impl CursorLimit {
217 pub fn new(max_items: u64) -> Self {
219 Self {
220 meta: NodeMeta {
221 name: "limit".into(),
222 outs: vec![Port::u64("output")],
223 ins: vec![Slot::Wire(Port::u64("input"))],
224 },
225 max_items,
226 }
227 }
228}
229
230impl PolydatNode for CursorLimit {
231 fn meta(&self) -> &NodeMeta {
232 &self.meta
233 }
234 fn eval(&self, inputs: &[Value], outputs: &mut [Value]) {
235 outputs[0] = inputs[0].clone();
239 }
240 fn compiled_u64(&self) -> Option<crate::ast::CompiledU64Op> {
243 Some(Box::new(|inputs: &[u64], outputs: &mut [u64]| {
244 outputs[0] = inputs[0];
245 }))
246 }
247}
248
249use crate::dsl::registry::{Arity, FuncCategory, FuncSig, ParamSpec};
256
257pub fn signatures() -> &'static [FuncSig] {
259 use FuncCategory as C;
260 &[FuncSig {
261 name: "limit",
262 category: C::Context,
263 outputs: 1,
264 description: "cursor limit — clamps extent for smoke testing",
265 help: "Passes through the input value unchanged. Inserted by the compiler\n\
266 when the `limit` activity parameter is present. The max_items value\n\
267 is used by the cursor system to stop advancing early.\n\
268 Parameters:\n input — cursor wire (u64)\n max_items — maximum items to yield\n\
269 Example: row = limit(row, 100) // stop after 100 items",
270 identity: None,
271 variadic_ctor: None,
272 params: &[
273 ParamSpec {
274 name: "input",
275 slot_type: SlotType::Wire,
276 required: true,
277 example: "row",
278 constraint: None,
279 },
280 ParamSpec {
281 name: "max_items",
282 slot_type: SlotType::ConstU64,
283 required: true,
284 example: "100",
285 constraint: None,
286 },
287 ],
288 arity: Arity::Fixed,
289 commutativity: crate::ast::Commutativity::Positional,
290 default_resolver: None,
291 output_type: crate::dsl::registry::OutputType::Fixed,
292 output_port: None,
295 }]
296}
297
298pub(crate) fn build_node(
301 name: &str,
302 _wires: &[crate::compile::assembly::WireRef],
303 _wire_types: &[crate::ast::PortType],
304 consts: &[crate::dsl::factory::ConstArg],
305) -> Option<Result<Box<dyn crate::ast::PolydatNode>, String>> {
306 match name {
307 "limit" => {
308 let max_items = consts.first().map(|c| c.as_u64()).unwrap_or(u64::MAX);
309 Some(Ok(Box::new(CursorLimit::new(max_items))))
310 }
311 _ => None,
312 }
313}
314
315crate::register_nodes!(signatures, build_node);
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 #[test]
322 fn current_epoch_millis_reasonable() {
323 let node = CurrentEpochMillis::new();
324 let mut out = [Value::None];
325 node.eval(&[], &mut out);
326 let millis = out[0].as_u64();
327 assert!(millis > 1_704_067_200_000);
329 }
330
331 #[test]
332 fn session_start_frozen() {
333 let node = SessionStartMillis::new();
334 let mut out1 = [Value::None];
335 let mut out2 = [Value::None];
336 node.eval(&[], &mut out1);
337 node.eval(&[], &mut out2);
338 assert_eq!(out1[0].as_u64(), out2[0].as_u64());
339 }
340
341 #[test]
342 fn elapsed_grows() {
343 let node = ElapsedMillis::new();
344 let mut out = [Value::None];
345 node.eval(&[], &mut out);
346 let e1 = out[0].as_u64();
347 assert!(e1 < 1000, "elapsed should be small right after creation");
349 }
350
351 #[test]
352 fn counter_increments() {
353 let node = Counter::new(0);
354 let mut out = [Value::None];
355 node.eval(&[], &mut out);
356 assert_eq!(out[0].as_u64(), 0);
357 node.eval(&[], &mut out);
358 assert_eq!(out[0].as_u64(), 1);
359 node.eval(&[], &mut out);
360 assert_eq!(out[0].as_u64(), 2);
361 }
362
363 #[test]
364 fn counter_starting_at() {
365 let node = Counter::new(100);
366 let mut out = [Value::None];
367 node.eval(&[], &mut out);
368 assert_eq!(out[0].as_u64(), 100);
369 node.eval(&[], &mut out);
370 assert_eq!(out[0].as_u64(), 101);
371 }
372
373 fn unique_var(tag: &str) -> String {
378 use std::time::{SystemTime, UNIX_EPOCH};
379 let nanos = SystemTime::now()
380 .duration_since(UNIX_EPOCH)
381 .unwrap()
382 .as_nanos();
383 format!("__NBRS_TEST_{tag}_{nanos:x}")
384 }
385
386 #[test]
387 fn env_captures_value_at_construction() {
388 let var = unique_var("ENV");
389 unsafe {
390 std::env::set_var(&var, "captured-value");
391 }
392 let node = Env::try_new(var.clone()).expect("env should read the set var");
393 unsafe {
396 std::env::set_var(&var, "later-value");
397 }
398 let mut out = [Value::None];
399 node.eval(&[], &mut out);
400 assert_eq!(out[0].as_str().to_string(), "captured-value");
401 unsafe {
402 std::env::remove_var(&var);
403 }
404 }
405
406 #[test]
407 fn env_errors_when_var_unset() {
408 let var = unique_var("ENV_MISSING");
409 unsafe {
410 std::env::remove_var(&var);
411 }
412 match Env::try_new(var.clone()) {
413 Ok(_) => panic!("Env::try_new should fail when the var is unset"),
414 Err(err) => {
415 assert!(
416 err.contains(&var),
417 "error should name the missing var: {err}"
418 );
419 assert!(
420 err.contains("env_or"),
421 "error should suggest env_or as the defaulted alternative: {err}"
422 );
423 }
424 }
425 }
426
427 #[test]
428 fn env_or_uses_default_when_var_unset() {
429 let var = unique_var("ENV_OR_MISSING");
430 unsafe {
431 std::env::remove_var(&var);
432 }
433 let node = EnvOr::new(var.clone(), "fallback".to_string());
434 let mut out = [Value::None];
435 node.eval(&[], &mut out);
436 assert_eq!(out[0].as_str().to_string(), "fallback");
437 }
438
439 #[test]
440 fn env_or_uses_var_value_when_set() {
441 let var = unique_var("ENV_OR_SET");
442 unsafe {
443 std::env::set_var(&var, "real-value");
444 }
445 let node = EnvOr::new(var.clone(), "fallback".to_string());
446 let mut out = [Value::None];
447 node.eval(&[], &mut out);
448 assert_eq!(out[0].as_str().to_string(), "real-value");
449 unsafe {
450 std::env::remove_var(&var);
451 }
452 }
453
454 #[test]
455 fn env_or_captures_at_construction_not_each_eval() {
456 let var = unique_var("ENV_OR_FROZEN");
457 unsafe {
458 std::env::set_var(&var, "first");
459 }
460 let node = EnvOr::new(var.clone(), "ignored-default".to_string());
461 unsafe {
462 std::env::set_var(&var, "second");
463 }
464 let mut out = [Value::None];
465 node.eval(&[], &mut out);
466 assert_eq!(
467 out[0].as_str().to_string(),
468 "first",
469 "env_or must freeze its value at construction; later env mutations are invisible"
470 );
471 unsafe {
472 std::env::remove_var(&var);
473 }
474 }
475
476 #[test]
477 fn tmp_dir_returns_a_path() {
478 let node = TmpDir::new();
479 let mut out = [Value::None];
480 node.eval(&[], &mut out);
481 let s = out[0].as_str().to_string();
482 assert!(!s.is_empty(), "tmp_dir() should produce a non-empty path");
483 }
484
485 #[test]
486 fn tmp_dir_is_stable_across_evals() {
487 let node = TmpDir::new();
488 let mut a = [Value::None];
489 let mut b = [Value::None];
490 node.eval(&[], &mut a);
491 node.eval(&[], &mut b);
492 assert_eq!(a[0].as_str(), b[0].as_str());
493 }
494
495 #[test]
498 fn env_or_compiles_through_dsl() {
499 let var = unique_var("DSL_ENV_OR");
500 unsafe {
501 std::env::set_var(&var, "x-value");
502 }
503 let src = format!("v := env_or(\"{var}\", \"fallback\")\n",);
504 let kernel = crate::dsl::compile_polydat(&src).expect("compile env_or");
505 unsafe {
506 std::env::remove_var(&var);
507 }
508 let names = kernel.program().output_names();
513 assert!(names.contains(&"v"), "expected output 'v' in {names:?}");
514 }
515
516 #[test]
517 fn tmp_dir_compiles_through_dsl_in_string_template() {
518 let src = "path := \"{tmp_dir()}/data\"\n";
523 let kernel =
524 crate::dsl::compile_polydat(src).expect("compile tmp_dir() interpolated in a string");
525 let names = kernel.program().output_names();
526 assert!(
527 names.contains(&"path"),
528 "expected output 'path' in {names:?}"
529 );
530 }
531}