polydat_core/library/
context.rs1use std::sync::atomic::{AtomicU64, Ordering};
24use std::time::{SystemTime, UNIX_EPOCH};
25
26#[crate::polydat_node(
30 category = Context,
31 purity = Nondeterministic("reads system clock"),
32)]
33fn current_epoch_millis() -> u64 {
34 SystemTime::now()
35 .duration_since(UNIX_EPOCH)
36 .unwrap()
37 .as_millis() as u64
38}
39
40#[crate::polydat_node(
45 category = Context,
46 purity = Nondeterministic("OS thread identity varies across fibers"),
47)]
48fn thread_id() -> u64 {
49 thread_local! {
50 static THREAD_ID: u64 = {
53 let id = std::thread::current().id();
54 let id_str = format!("{id:?}");
55 let num = id_str.trim_start_matches("ThreadId(").trim_end_matches(')');
56 num.parse().unwrap_or(0)
57 };
58 }
59 THREAD_ID.with(|id| *id)
60}
61
62#[crate::polydat_node(category = Context)]
74fn env(name: Const<&str>) -> Result<String, String> {
75 let var = name.0;
76 std::env::var(var).map_err(|_| {
77 format!(
78 "env('{var}'): environment variable not set; \
79 use env_or('{var}', '<default>') if a fallback is acceptable",
80 )
81 })
82}
83
84#[crate::polydat_node(category = Context)]
91fn env_or(
92 name: Const<&str>,
93 default: Const<&str>,
94 #[poly_const(capture_env_opt, from = name)] captured: &Option<String>,
95) -> String {
96 match captured {
97 Some(v) => v.clone(),
98 None => default.0.to_string(),
99 }
100}
101
102fn capture_env_opt(name: &str) -> Option<String> {
105 std::env::var(name).ok()
106}
107
108#[crate::polydat_node(category = Context)]
112fn tmp_dir(#[poly_const(capture_tmp_dir, from = ())] path: &String) -> String {
113 path.clone()
114}
115
116fn capture_tmp_dir() -> String {
120 std::env::temp_dir()
121 .to_str()
122 .map(String::from)
123 .unwrap_or_else(|| "/tmp".to_string())
124}
125
126#[crate::polydat_node(
130 category = Context,
131 purity = Nondeterministic("monotonic counter incremented per call"),
132)]
133fn counter(
134 #[poly_default(0u64)] start: Const<u64>,
135 #[poly_const(AtomicU64::new, from = start)] count: &AtomicU64,
136) -> u64 {
137 count.fetch_add(1, Ordering::Relaxed)
138}
139
140#[crate::polydat_node(category = Context)]
150fn limit(input: u64, max_items: Const<u64>) -> u64 {
151 let _ = max_items;
152 input
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use crate::ast::{PolydatNode, Value};
159
160 #[test]
161 fn current_epoch_millis_reasonable() {
162 let node = CurrentEpochMillis::new();
163 let mut out = [Value::None];
164 node.eval(&[], &mut out);
165 let millis = out[0].as_u64();
166 assert!(millis > 1_704_067_200_000);
168 }
169
170 #[test]
171 fn counter_increments() {
172 let node = Counter::new(0);
173 let mut out = [Value::None];
174 node.eval(&[], &mut out);
175 assert_eq!(out[0].as_u64(), 0);
176 node.eval(&[], &mut out);
177 assert_eq!(out[0].as_u64(), 1);
178 node.eval(&[], &mut out);
179 assert_eq!(out[0].as_u64(), 2);
180 }
181
182 #[test]
183 fn counter_starting_at() {
184 let node = Counter::new(100);
185 let mut out = [Value::None];
186 node.eval(&[], &mut out);
187 assert_eq!(out[0].as_u64(), 100);
188 node.eval(&[], &mut out);
189 assert_eq!(out[0].as_u64(), 101);
190 }
191
192 fn unique_var(tag: &str) -> String {
197 use std::time::{SystemTime, UNIX_EPOCH};
198 let nanos = SystemTime::now()
199 .duration_since(UNIX_EPOCH)
200 .unwrap()
201 .as_nanos();
202 format!("__NBRS_TEST_{tag}_{nanos:x}")
203 }
204
205 #[test]
206 fn env_captures_value_at_construction() {
207 let var = unique_var("ENV");
208 unsafe {
209 std::env::set_var(&var, "captured-value");
210 }
211 let node = Env::try_new(var.clone()).expect("env should read the set var");
212 unsafe {
215 std::env::set_var(&var, "later-value");
216 }
217 let mut out = [Value::None];
218 node.eval(&[], &mut out);
219 assert_eq!(out[0].as_str().to_string(), "captured-value");
220 unsafe {
221 std::env::remove_var(&var);
222 }
223 }
224
225 #[test]
226 fn env_errors_when_var_unset() {
227 let var = unique_var("ENV_MISSING");
228 unsafe {
229 std::env::remove_var(&var);
230 }
231 match Env::try_new(var.clone()) {
232 Ok(_) => panic!("Env::try_new should fail when the var is unset"),
233 Err(err) => {
234 assert!(
235 err.contains(&var),
236 "error should name the missing var: {err}"
237 );
238 assert!(
239 err.contains("env_or"),
240 "error should suggest env_or as the defaulted alternative: {err}"
241 );
242 }
243 }
244 }
245
246 #[test]
247 fn env_or_uses_default_when_var_unset() {
248 let var = unique_var("ENV_OR_MISSING");
249 unsafe {
250 std::env::remove_var(&var);
251 }
252 let node = EnvOr::new(var.clone(), "fallback".to_string());
253 let mut out = [Value::None];
254 node.eval(&[], &mut out);
255 assert_eq!(out[0].as_str().to_string(), "fallback");
256 }
257
258 #[test]
259 fn env_or_uses_var_value_when_set() {
260 let var = unique_var("ENV_OR_SET");
261 unsafe {
262 std::env::set_var(&var, "real-value");
263 }
264 let node = EnvOr::new(var.clone(), "fallback".to_string());
265 let mut out = [Value::None];
266 node.eval(&[], &mut out);
267 assert_eq!(out[0].as_str().to_string(), "real-value");
268 unsafe {
269 std::env::remove_var(&var);
270 }
271 }
272
273 #[test]
274 fn env_or_captures_at_construction_not_each_eval() {
275 let var = unique_var("ENV_OR_FROZEN");
276 unsafe {
277 std::env::set_var(&var, "first");
278 }
279 let node = EnvOr::new(var.clone(), "ignored-default".to_string());
280 unsafe {
281 std::env::set_var(&var, "second");
282 }
283 let mut out = [Value::None];
284 node.eval(&[], &mut out);
285 assert_eq!(
286 out[0].as_str().to_string(),
287 "first",
288 "env_or must freeze its value at construction; later env mutations are invisible"
289 );
290 unsafe {
291 std::env::remove_var(&var);
292 }
293 }
294
295 #[test]
296 fn tmp_dir_returns_a_path() {
297 let node = TmpDir::new();
298 let mut out = [Value::None];
299 node.eval(&[], &mut out);
300 let s = out[0].as_str().to_string();
301 assert!(!s.is_empty(), "tmp_dir() should produce a non-empty path");
302 }
303
304 #[test]
305 fn tmp_dir_is_stable_across_evals() {
306 let node = TmpDir::new();
307 let mut a = [Value::None];
308 let mut b = [Value::None];
309 node.eval(&[], &mut a);
310 node.eval(&[], &mut b);
311 assert_eq!(a[0].as_str(), b[0].as_str());
312 }
313
314 #[test]
317 fn env_or_compiles_through_dsl() {
318 let var = unique_var("DSL_ENV_OR");
319 unsafe {
320 std::env::set_var(&var, "x-value");
321 }
322 let src = format!("v := env_or(\"{var}\", \"fallback\")\n",);
323 let kernel = crate::dsl::compile_polydat_interpreter(&src).expect("compile env_or");
324 unsafe {
325 std::env::remove_var(&var);
326 }
327 let names = kernel.program().output_names();
332 assert!(names.contains(&"v"), "expected output 'v' in {names:?}");
333 }
334
335 #[test]
336 fn tmp_dir_compiles_through_dsl_in_string_template() {
337 let src = "path := \"{tmp_dir()}/data\"\n";
342 let kernel = crate::dsl::compile_polydat_interpreter(src)
343 .expect("compile tmp_dir() interpolated in a string");
344 let names = kernel.program().output_names();
345 assert!(
346 names.contains(&"path"),
347 "expected output 'path' in {names:?}"
348 );
349 }
350}